错误示例:
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){ // 1
synchronized (Singleton.class){ // 2
if(singleton == null){ // 3
singleton = new Singleton(); // 4
}
}
}
return singleton;
}}
首先我们知道实例化一个对象有3个步骤:
-
分配内存空间
-
初始化对象
-
将内存空间的地址赋值给对应的引用
因为JVM存在指令重排,有可能2和3进行了重新排序,先执行了2.将内存空间的地址赋值给对应的引用。那么在多线程的情况下将导致singleton!=null, 那么就有可能发生灾难性的后果。
如何才能保证2,3的执行顺序不发生变化,保证安全呢? 只需要在静态变量前面加个volatile关键字即可。因为volatile关键字能够保证初始化对象优先于将内存空间的地址赋值给对应的引用的操作。即保证了操作不发生重排序, 这个保证是依靠jvm对volatile底层的实现-------插入了对应的内存屏障来保证不发生重排序。这是其中的一种单例实现方案。
还有第二中可以依靠类加载机制来保证单例模式在多线程下的执行。
public class Singleton {
private static class SingletonHolder{
public static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.singleton;
}}
这种方式实际上是利用了classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
Java语言规定,对于每一个类或者接口C,都有一个唯一的初始化锁LC与之相对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。