public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton singleton;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getInstance() {
if (singleton == null) {
synchronized (DoubleCheckSingleton.class) {
if (singleton == null) {
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
- 第一次判空 在同步代码块外部进行判断,在单例已经创建的情况下,避免进入同步代码块,提升效率
- 第二次判空 为了避免创建多个单例。假设线程1首先通过第一次判空,还未获得锁时时间片就用完了,此时,线程2获得CPU时间片,并调用单单例方法,此时singleton仍然为空,于是线程2顺利创建了singleton对象。稍后,线程1获得时间片,由于已经执行过了第一层判空,此时如果没有第二次判空,那么线程1也会再创建一个singleton实例,即不满足单例的要求。所以第二此判空很有必要。
编译器为了优化程序性能,可能会在编译时对字节码指令进行重排序。重排序后的指令在单线程中运行时没有问题的,但是如果在多线程中,重排序后的代码则可能会出现问题。
双重锁校验中实例化singleton的代码在编译成字节指令后并不是一个原子操作,而是会分为三个指令:
- 1.分配对象内存:memory = allocate();
- 2.初始化对象:instance(memory);
- 3.instance指向刚分配的内存地址:instance = memory;
但是由于编译器指令重排序,上述指令可能会出现以下顺序:
- 1.分配对象内存:memory = allocate();
- 2.instance指向刚分配的内存地址:instance = memory;
- 3.初始化对象:instance(memory);
假设这个singleton的实例化经过了编译器的重排序,此时有一个线程1调用了该单例方法,并在执行完上述第2步后用完了CPU事件片,此时singleton对象实际上还没有被初始化,但是singleton却被赋值指向了第一步分配的内存。此时,一个线程2获得CPU时间片,并调用这个单例方法,发现singleton不为null,随即return了singleton,但singleton实际上并没有初始化,因此可能造成程执行序异常。
如果给成员变量加了volatile关键字,那么编译器便不会对其进行指令重排序,也就不会出现上边的问题。
既能保证线程安全,又能实现延迟加载。缺点时使用synchronized关键字会影响性能。
public class StaticSingleton {
private StaticSingleton singleton;
private StaticSingleton() {
}
private static class SingletonHolder {
public static StaticSingleton INSTANCE = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种单例利用了类加载的特性,在《Java虚拟机规范》中对于类初始化的时机有着严格的约束:
① 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
② 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
③ 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
④ 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
⑤ 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
⑥ 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
从上述类初始化的条件可以看出,如果不调用SingletonHolder.INSTANCE,SingletonHolder类就不会被加载到虚拟机,SingletonHolder不被加载到虚拟机那么INSTANCE实例也不会被实例化。
而当调用了getInstance后会调用SingletonHolder.INSTANCE,这里是一个getstatic指令,所有会触发SingleHolder的初始化,初始化前会进行SingletonHolder的类加载,在类加载的初始化过程中INSTANCE会被实例化。
// 枚举单例
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("通过枚举单利打印日志...");
}
}
// 测试类
public class Test {
public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
}
}
枚举实现的单利是最完美的一种方式。这种方式可以防止序列化与反序列化造成创建多个实例的问题,而前面的几种方式都无法解决这个问题。