先看一下传统的double-check模式,代码如下:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
读《Java 多线程编程实战指南》一书,我们知道,在 instance = new Singleton()这一步的代码,并不是一个原子操作, 它往往被分解为以下三个独立的子操作:
- objRef = allocate(Singleton.class) //1、分配对象所需要的存储空间
- invokeConstructor(objRef) //2、初始化objRef所引用的对象
- instance = objRef //3、将引用写入instance变量
临界区内的操作在临界区内部是可以被重排序的,那么,JIT编译器是可能将上述操作的第2步和第3步重排序,即在初始化这个 对象之前将对象的引用写入instance变量中。而double-check的第一次检查,读取instance变量并没有加锁,导致另一个 线程可能看到一个未初始化(或者说未初始化完毕)的实例,变量instance的值不为空,但是instance引用的对象中的某些 其他实例变量的值并不是构造器中设置的初始值,而是一些默认值;因此该线程返回的instance变量所引用的实例是一个 未初始化完毕的,导致出错。即代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。
所以我们在instance变量前加了volatile修饰符,在这里,volatile保证了可见性和有序性:
- 可见性:一个线程通过执行instance = new Singleton()方法修改了instance变量的值,其他线程能够读取到这个值。
- 有序性:volatile禁止了 volatile变量写操作与该操作之前任何的读写操作进行重排序,因此禁止了JIT编译器以及处理器 在上面的重排序,这保证了另一个线程读取到的instance变量引用的实例已经初始化完毕。
上面我们使用volatile的方式来保证线程安全,这是因为禁止了重排序,下面这种方案换了一个思路:允许2和3重排序,但不允许 其他线程“看到”这个重排序。
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁, 这个锁可以同步多个线程对同一个类的初始化。
使用静态内部类的方式会比较简单,代码如下
public class Singleton {
private Singleton() {}
private static class InstanceHolder {
final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.INSTANCE;
}
}
这是因为类的静态变量只会创建一次,调用getInstance()方法时,JVM会初始化这个方法所访问的静态内部类,这使得 InstanceHolder的静态变量INSTANCE会被初始化(这个静态变量应该一直存储在方法区中),会被多个线程共享。
attention::静态内部类不会在Singleton类加载的时候就加载,而是在调用getInstance()方法的时候才会加载。
当然,上面这种通过静态内部类的方式不严格正确,可以通过反射的方式设置constructor的访问属性为true,并构造多个 实例对象。也可以通过反序列化方式来构造出不同的对象。具体方法参考这篇文章 如何通过反射和反序列化攻击单例模式
下面我们来看目前最佳的也是最简洁的实现单例模式的方法:
public enum SingletonEnum {
INSTANCE;
SingletonEnum() {
}
public void doSomething() {
}
}
直接通过SingletonEnum.INSTANCE即可访问这个instance对象。