手写普通的单例模式要点有三个:
getInstance()
获取单例对象懒汉模式指的是单例对象的延迟加载,只有在调用 getInstance()
获取单例对象时才会将单例创建出来。懒汉模式适用于对内存要求高的场景。代码如下:
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
与懒汉模式相对的是饿汉模式,适用于对内存要求不高的场景,在类加载的初始化阶段就完成了单例对象的创建,代码如下:
public class Singleton { // 静态变量初始化 private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
静态变量的初始化是在类加载阶段的初始化过程进行,在此期间,编译器会自动收集类中所有静态变量的赋值动作和 static
块,生成 <clinit>
方法并执行。比较特殊的一点是,如果多个线程同时初始化 Singleton
类,JVM 会保证只有一个线程能够执行 Singleton
类的 <clinit>
方法,其他线程都必须阻塞等待。而且同一个类加载器下,一个类只会被初始化一次,即 <clinit>
方法只会被执行一次,这就保证了多线程下单例对象只会被创建一次
单例模式需要保证的一点是,在整个程序运行期间,单例对象只会被创建一次。如果是单线程环境中,这一点很好保证。但如果是多线程环境中,保证这一点并不简单
上面已经说过,饿汉模式的单例模式下,JVM 会保证单例对象只会被创建一次,因此可以保证这一点。而懒汉模式在多线程环境中不能保证这一点,接下来讨论的是对懒汉模式进行改造,让它能够保证这一点
最简单直接的方式就是为 getInstance()
加上 synchronized
关键字,这样确实可以保证多线程环境中,单例对象只会被创建一次。但是 synchronized
方法最大的缺点在于它将获取单例对象这一行为彻底串行化,同一时刻只能有一个线程能执行 getInstance()
,大大降低了并发效率
代码如下:
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
直接使用 synchronized
方法降低效率的主要原因在于,synchronized
方法的加锁粒度太粗,那么将锁的范围缩小,就可以缓解这一问题,而双重检测锁就是这么实现的。不过为了保证并发的正确性,在内部又加了一道检测,故名为双重检测锁。代码如下:
public class Singleton { // 这里的instance一定要定义为volatile变量!!! private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { // 双重锁检测 if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
上面代码的关键点有三个:
synchronized
加锁的范围更小,这是为了更高的并发效率synchronized
内部还有一道检测,如果线程1进入了同步块,但还未将单例对象创建出来,此时线程2正好绕过了第一道检测,在同步块外等待获取锁定。因此同步块内也要加上一道检测,避免单例对象被重复创建instance
这个变量一定要声明为 volatile
!volatile
在这里最大的作用是禁止指令重排序。如果不加 volatile
修饰,由于 instance = new Singleton()
可能被重排序而导致在这条语句执行过程中,instance
率先被分配内存并获得地址,成为非 null,但构造函数却没有真正执行完毕,此时别的线程可能拿到的 instance
就是不完全构造的单例对象instance = new Singleton()
这条语句正常的执行顺序是:
1、为即将创建的对象分配一块内存
2、执行构造函数中的语句,对内存进行相应的读写操作
3、让 instance
指向这块内存
在重排序情况下顺序可能是 1 -> 3 -> 2,当执行到3时 instance
就成为非 null,此时其他线程如果引用了 instance
,拿到的就是一个不完全构造的对象
作者:酒冽 出处:https://www.cnblogs.com/frankiedyz/p/15847802.html需要注意的是,在 JDK5 之前,就算加了
volatile
关键字也依然有问题,原因是之前的 JMM 是有缺陷,volatile
变量前后的代码仍然可以出现重排序问题,这个问题在 JDK5 之后才得到解决,所以现在才可以这么使用
基于枚举类的方式非常简洁,只要简单地编写一个只包含一个元素的枚举类,由 JVM 来保证单例的唯一性和线程安全性,自带私有的构造方法并且序列化和反射都不会破坏单例的唯一性,据说是 JDK5 之后最好的单例创建方式
public enum Singleton { instance; // 定义各种字段、方法 private Singleton() {} public static Singleton getInstance() { return instance; } }
其中枚举类的构造器不用特意加上 private
修饰,因为枚举类构造器默认就是 private
的,且只能使用 private
修饰
简单理解枚举实现单例的过程:程序启动时,会自动调用
Singleton
的构造器,实例化单例对象并赋给instance
,之后再也不会实例化,这也是一个饿汉过程,即使没有调用过getInstance()
,也会将单例对象创建出来
使用枚举来创建单例模式的优势有3点:
这第三点优势让基于枚举类的单例模式变得“无懈可击”了,枚举类可以保证唯一性的原理如下:
枚举类默认继承了 Enum
类,在利用反射调用 newInstance()
时,会判断该类是否是枚举类,如果是则抛出异常
对于枚举类型,由于枚举类和枚举变量的组合名是唯一的,可以唯一确定对象。因此,序列化只会将枚举类名 + 枚举变量名输出到文件中。反序列化时,读入的就是枚举类名 + 枚举变量名,再根据 Enum
类的 valueOf
方法,在内存中找对已经存在的枚举对象,并不会创建新的对象
同一个类加载器对一个类只会加载一次,但是不同的类加载器可能会多次加载同一个类,如果程序中有多个类加载器,需要在单例中指定某个特定的类加载器,并保证这个类加载器始终是同一个