Talk is cheap. Show me the code.
public class Singleton {
private volatile static Singleton instance= null;
private Singleton() {};
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这是业届奉为经典的 “双重检验锁-懒汉单例”,相信熟悉 Java 语法的人都能看懂,通过这段代码,我们实现了一个单例类,单例类的定义如下,
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
通过代码可以看出,由于构造函数私有(private),我们获取 Singleton 类的实例对象只能通过 Singleton.getInstance() 的方式,而无法在外界通过 new 或其他方式创建此类的实例对象,并且由于此成员变量被 static 修饰,使得实例对象属于类本身且只有唯一一个。
也许你已经看懂了这段代码的整体结构,但这段经典代码中仍有几点细节值得思考,比如 volatile,synchronized 和两次出现的 if (instance == null)。
我想,这几个关键字并不陌生。
是的没错,这正是因为我们要保证在并发情况下的安全性问题。
先讲 synchroized ,如果没有 synchronized,
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
在多线程情况下,对于 instance == null 的判断会出现同时多个线程识别到 instance 为 null,同时进行多个实例的创建,即使最终只会有一个实例被引用,可这与我们单例模式设计的初衷并不符合,并且造成大量的内存空间浪费,显然很不合理。
那么如果对方法加上 synchronized 的呢?
public class Singleton {
private volatile static Singleton instance= null;
private Singleton() {};
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这样保证只有一个线程进入 getInstance() 方法调用,执行判断与返回,是可以成功解决并发的安全性问题的!但是缺点在于锁的粒度太大了,多个线程同时调用 getInstance() 方法时,除一个线程之外,剩下所有线程都会被阻塞。我只是想读取一下呀,看看也不行么?我们更希望如果 instance 对象存在的话,每个线程都可以直接返回实例对象,所以让我们缩小同步代码的范围。
public class Singleton {
private volatile static Singleton singelton = null;
private Singleton() {};
public static Singleton getSingleton() {
if (singleton == null) {
synchronized(Singleton.class) {
singleton = new Singleton();
}
}
return singelton;
}
}
哈哈哈,是不是合理了很多,线程调用 getInstance() 方法时,只有在发现 instance 为 null 的情况下才会获取锁对象,阻塞其他进程,进行对象的实例化操作。如果 instance 不是 null 的话就直接返回啦。
看到这里,有没有忽然间想起点什么,我们代码的名字,双重检验呀有木有!
public static Singleton getInstance() {
// 第一次检验
if (instance == null) {
synchronized(Singleton.class) {
// 第二次检验
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
第一次检验是在线程执行 getInstance() 方法时,如果不为 null 就直接返回。那么第二次检验如代码所示,位于 synchronized 修饰的代码块中。
这是由于假设此刻 instance 为 null,如果A,B两个线程同时判断 instance == null 成立,那么两个线程都会进行锁资源的争夺,如果 A 获取到锁资源,则 B 进行阻塞,待 A 完成实例化操作释放掉锁资源后,B 被唤醒,而此刻必须重新判断 instance 的状态,否则 B 会依旧认为 instance 为 null,进行实例化操作,创建新的对象,那么便违背了单例模式只有一个实例对象的原则。
到此为止,我们已经搞懂了 synchronized 和 双重检验,只剩下一个小小的疑问,为什么要加 volatile 呢?
private volatile static Singleton singelton = null;
这就不得不提到有关 java 源码编译后指令执行顺序的两个知识点:
- instance = new Singleton() 在编译后会被分解为 3 个指令。
- volatile 的功能之一:禁止指令重排序
先说 instance = new Singleton() 会被分解为三个步骤,
- memory=allocate(); // 分配内存 相当于c的malloc
- ctorInstanc(memory) //初始化对象
- instance=memory //设置instance指向刚分配的地址
而 JVM (Java 虚拟机) 可能会对这三个指令进行重排序,将指令顺序重排为 1→3→2。
那么可能出现这样一种情况,A线程正在执行 instance = new Singleton() 中的 3 指令,即分配完内存空间,并将 instance 指向此内存空间,如果此时恰巧有一个 B 线程执行 getInstance() 方法,会判断 instance 不是 null,将 instance 返回,那么就会返回一个未初始化的对象,造成程序错误。
而用 volatile 就可以完美的解决这个问题,因为被 volatile 修饰的 instance 属性,会在操作其前后设置内存屏障(详见volatile原理),达到禁止其相关指令重排序的功能,使得 instance 一定会被初始化,避免了上述问题。
好啦~,以上就是本文的全部内容了,希望读完的你能够有所收获呀!
1)记住 “双重检验锁-懒汉单例” 的写法。
2)明白为什么 synchronized 在方法内使用。
3)理解双重检验中每次检验的意义。
4)搞懂 volatile 的使用原因。
参考资料:
双重检验的单例模式,为什么要用volatile关键字
双重检验锁思考
《Java并发编程之美》