单例模式是指一个类只能构建一个对象的设计模式。单例模式根据构建对象的时间分为两类:启动时就构建好的饿汉式和初次被调用才构建的懒汉式。实现一个单例模式主要需要考虑的是线程安全问题,另外是否能懒加载和是否能防止反射构建也是需要考虑的一部分。
双重检测锁实现
双重检测锁是单例模式的一种实现,可以满足线程安全和懒加载的需要,不过不能防止利用反射来构建多个对象。
基本实现
首先我们从最简单的代码开始一步步构建双重检测锁的单例实现:
1 | public class Singleton { |
- 首先第一步,我们是把构造方法给私有化,防止外部随意调用构造方法构建对象;
instance
便是我们的单例对象,因为是一个静态成员,赋值为null
实现了懒加载,当然也可以写成new Singleton()
实现饿汉式加载;getInstance()
是获取单例对象的方法,通过if (instance == null)
这一步来确保对象只会被构建一次。
确保线程安全
上面的基本实现能保证在单线程下实现单例模式,但在多线程的情况下可能会出现一些问题:
刚开始instance
还是等于null
的时候,if (instance == null)
这一步判断是为true
,当线程A通过了这一步判断进入了分支里但还没来得及构建对象的时候,instance
实际上还是为null
,这时候线程B依然能通过判断进入分支体,于是就会出现多个线程同时在分支体里面,导致分支体里的instance = new Singleton();
会被执行多次,构建了多次对象。
添加双重检测机制:
1 | public class Singleton { |
- 通过添加同步锁
synchronized
的方式,保证同一时刻最多只会有一个线程在执行同步锁里的程序,保证instance = new Singleton();
只会被执行一次; - 同步锁外的
if (instance == null)
判空是指当不是null
的时候就直接跳过整个分支体,而不用等待完同步锁后才发现instance
不为null
,减少不必要的时间开销; - 同步锁里的
if (instance == null)
判空是为了防止线程A进入了锁里面还没来得及创建对象时,线程B就通过了外部判空开始等待锁的情况出现,这种机制就叫做双重检测锁。
禁止指令重排序
上面的实现看似保证了线程安全,但实际上并非如此,原因在于Java指令的重排序。
什么是指令重排序?上面构建对象部分的代码instance = new Singleton();
,实际上会被编译器编译成以下指令:
- 分配对象需要的内存空间
- 在分配的内存中初始化对象
- 把
instance
指向上面分配到的内存地址
指令流水线并非是串行的,多条指令可以同时被执行,为了使性能更优,JVM和CPU可能会把这些指令进行优化重排序,导致出现以下的顺序:
- 分配对象需要的内存空间
- 把
instance
指向上面分配到的内存地址 - 在分配的内存中初始化对象
结果会导致这样一种情况:线程A正在执行instance = new Singleton()
,获取了内存地址但是还没有初始化对象,而此时线程B在外部判空时发现instance
不等于null
,于是直接返回了instance
,便造成线程B拿到了一个还没完成初始化的对象。
添加修饰符volatile:
1 | public class Singleton { |
使用volatile修饰符禁止了指令重排序,保证指令是按顺序执行的,不会出现一种中间态。另外一提,volatile关键字除了禁止指令重排外,还能确保其修饰的值被修改后马上写回到主内存中,保证线程访问的该变量值是最新值。
至此为止,代码已经实现了线程安全且懒加载的单例模式,然而,硬要用反射来使构造函数可见,还是能构建多个对象的。