单例模式详解
Posted 程序员囧辉
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式详解相关的知识,希望对你有一定的参考价值。
概述
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,使用单例模式的类只有一个对象实例。
单例应用场景
Windows系统的任务管理器。
Windows系统的回收站。
操作系统的文件系统,一个操作系统只能有一个文件系统。
数据库连接池的设计与实现。
多线程的线程池设计与实现。
Spring中创建的Bean实例默认都是单例。
Java-Web中,一个Servlet类只有一个实例。
等等...
单例的实现要点
单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法)。
单例的实现主要是通过以下三个步骤:
将类的构造方法定义为私有方法。这样其他类的代码就无法通过调用该类的构造方法来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
定义一个私有的类的静态实例。
提供一个公有的获取实例的静态方法。
单例追求的目标
线程安全。
懒加载。
调用效率高。
单例模式的常用写法
常用的单例有以下5种写法,如果还有其他的写法,也基本是从以下5种稍微修改而来,由于内容基本一致,并且可能不是很常用,因此在本文中不再赘述。
1.饿汉模式
饿汉模式,比较常见的一种写法。在类加载的时候就对实例进行初始化,没有线程安全问题;获取实例的静态方法没有使用同步,调用效率高;但是没有使用懒加载,如果该实例从始至终都没被使用过,则会造成内存浪费。
总结:线程安全、非懒加载、效率高。
是否推荐:可以使用,但不推荐。
2.懒汉模式
线程安全的懒汉模式,比较常见的一种写法。在第一次使用的时候才进行初始化,达到了懒加载的效果;由于获取实例的静态方法用synchronized修饰,所以也没有线程安全的问题;但是,这种写法每次获取实例都要进行同步(加锁),因此效率较低,并且可能很多同步都是没必要的。
总结:线程安全、懒加载、效率低。
是否推荐:可以使用,但不推荐。
注:该模式还有另一种常见写法,就是把getInstance方法上的synchronized去掉,这种方法有线程安全问题,不能使用。
3.双重检测机制(DCL)
双重检测机制(双重检查加锁),比较常见的一种写法。在第一次使用的时候才进行初始化,达到了懒加载的效果;在进行初始化的时候会进行同步(加锁),因此没有线程安全问题;并且只有第一次进行初始化才进行同步,因此不会有效率方面的问题。
《Java Concurrency in Practice》作者Brian Goetz在书中提到关于DCL的观点:促使DCL模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在,因而它不是一种高效的优化措施。延迟初始化占位类模式(静态内部类)能带来同样的优势,并且更容易理解。
总结:线程安全、懒加载、效率高。
是否推荐:可以使用。
注:该模式还有另一种常见写法,就是把静态实例singleton的volatile修饰去掉,这种方法有线程安全方面的问题,不能使用。在我的另一篇文章有提到这个:,这里直接截取该部分内容,请见下面这个例子。
例子:双重检测机制实现单例(没有volatile修饰)
这段代码是单例的双重检测机制实现,相信很多人都用过,并且觉得这个代码是没问题的。在大多数情况,这段代码确实没问题,但在极端的情况下,有个隐藏的问题。
例子分析:
假设有两个线程同时访问这段代码,此时线程A走到15行开始初始化对象,线程B则刚走到12行进行第一次检测。这时要介绍下15行初始化这行代码,这行代码虽然只有一句话,但是被编译后会变成以下3条指令:
正常情况下,这3条执行时按顺序执行,双重检测机制就没有问题。但是CPU内部会在保证不影响最终结果的前提下对指令进行重新排序(不影响最终结果只是针对单线程,切记),指令重排的主要目的是为了提高效率。在本例中,如果这3条指令被重排成以下顺序:
如果线程A执行完1和3,instance对象还未完成初始化,但是已经不再指向null。此时线程B抢占到CPU资源,执行第12行的检测结果为false,则执行第19行,从而返回一个还未初始化完成的instance对象,从而出导致问题出现。要解决这个问题,只需要使用volatile关键字修饰instance对象即可。
4.静态内部类(延迟初始化占位类)
静态内部类(延迟初始化占位类),比较常见的一种写法。JVM将推迟SingletonHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Singleton,因此不需要额外的同步。当任何一个线程第一次调用getInstance时,都会使SingletonHolder被加载和被初始化,此时静态初始化器将执行Singleton的初始化操作。
通过静态初始化来初始化Singleton为什么不需要额外的同步?
在初始器中采用了特殊的方式来处理静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据破坏。
总结:线程安全、懒加载、效率高。
是否推荐:推荐使用、《Java Concurrency in Practice》作者Brian Goetz推荐使用的方式。
5.枚举
枚举,不是很常见的一种写法。很简洁的一种实现方式,提供了序列化机制,保证线程安全,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。
总结:线程安全、非懒加载、效率高。
是否推荐:推荐使用、《Effective Java》作者Joshua Bloch推荐使用的方式。
几种方式对比:
可能有人看了以上表格,觉得枚举有缺点,为什么Joshua Bloch还推荐使用枚举?
这就要提到单例的破解了。普通的单例模式是可以通过反射和序列化/反序列化来破解的,而Enum由于自身的特性问题,是无法破解的。当然,由于这种情况基本不会出现,因此我们在使用单例模式的时候也比较少考虑这个问题。
总结:
单例无论是在项目的开发中,还是面试中都是非常常见的,因此需要熟练的掌握好单例的知识。只要记住单例的三个实现要点——私有构造方法、定义静态Singleton实例、暴露实例获取方法,手写这5种实现方式相信都是信手拈来。如果是面试的时候,建议使用静态内部类的实现。
项目中具体使用哪种实现方式可以根据情况而定,个人推荐的方式是:静态内部类和枚举,我自己在项目中常见的也是这两种方式,可能静态内部类会更多一点。另外就是毕竟是两位大神推荐的,还考虑什么,直接用就是了。
—————END—————
以上是关于单例模式详解的主要内容,如果未能解决你的问题,请参考以下文章