我们先来看一段 Java 代码,DCL (Double Check Lock) 单例模式:
package singleton;
public class Mgr06 {
private volatile static Mgr06 INSTANCE;
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
synchronized (Mgr06.class) {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args){
for (int i = 0; i < 100; i++) {
new Thread(()->
// 输出实例的哈希值
System.out.println(Mgr06.getInstance().hashCode())
).start();
}
}
}
那么,在定义静态变量 INSTANCE 的时候,需不需要加 volatile 关键字呢?
我们来详细探究一下对象的创建过程:
首先,我们先例举一段简单的创建对象的代码,Java 代码如下:
package test;
public class T {
int m = 8;
public static void main(String args[]) {
T t = new T();
}
}
我们知道,Java 代码转化成二进制代码前,需要先编译成 Java字节码,如 Java 文件 T.java 会先被 JVM (Java 虚拟机) 编译成 T.class 。
汇编码如下:
0 new #2
3 dup
4 invokespecial #3 >
7 astore_1
8 return
该代码对象创建的示意图:
0 new #2
因为 m 为 int 类型,因此 m 的初始值为 m = 0 。该状态为半初始化状态。
4 invokespecial #3
7 astore_1 :建立对象 t 与类 T 之间的关联。
再看回原来的 DCL (Double Check Lock)单例:
如上图所示:
当线程 T1 拿到这把锁的时候(Java 代码如下)
synchronized (Mgr06.class)
在执行 INSTANCE = new Mgr06() 创建对象的过程中,在 Java 字节码里,创建对象的具体过程是:
假设不加关键字 volatile ,导致 Java 字节码的指令发生了重排序( volatile 关键字的用作之一:禁止指令重排序)
如上图所示,指令 astore_1 和指令 invokespecial #3
那么,就会造成指令执行到 astore_1 的时候,对象 INSTANCE 和类 Mgr06 已经建立了关联。
Java 代码里面有一个非空判断:
public static Mgr06 getInstance() {
if (INSTANCE == null) {
synchronized (Mgr06.class) {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
在第一个非空判断的时候,由于 INSTANCE 和类 Mgr06 已经发生了关联,那么就会造成 INSTANCE 不为空( INSTANCE == null 返回的结果为 false )
因此函数直接 return INSTANCE 。
但是,此时 INSTANCE 指向的是一个半初始化的对象,因为由于创建 INSTANCE 对象的 Java 字节码发生了指令重排序。
此时,还没有执行指令 invokespecial #3
那么,线程 T2 在执行 INSTANCE == null 条件判断语句的时候,条件判断返回的结果就会为 false (因为线程 T1 已经对该对象进行半初始化,该对象不为空)。
因此,线程 T2 直接执行:
return INSTANCE;
这样会造成线程 T2 返回了一个半初始化的对象,最终会影响程序执行的结果。因为,正确返回的结果应当是指令不发生重排序,正常初始化的对象。
因此,在 DCL 单例模式中,需要加 volatile 关键字,被 volatile 修饰的变量禁止指令重排序,这样可以防止在创建对象的过程中发生指令重排序,从而让程序运行的结果不被指令重排序所影响。