volatile 的使用
下面以一个单例模式,介绍 volatile 的使用:
public final class LazyDoubleCheckSingleton {
private static volatile LazyDoubleCheckSingleton instance;
Socket socket;
/**
* 实例创建过程分 3 步:
* 1.分配对象内存空间
* 2.初始化对象(执行构造器中的代码)
* 3.instance 指向刚分配的内存空间
*/
private LazyDoubleCheckSingleton() {
/**
* 如果 instance 没有声明成 volatile, 当 new LazyDoubleCheckSingleton() 时,
* 可能会因为指令重排序造成第 3 步先于第 2 步执行 -------------------------------------------- 启下
*/
socket = new Socket();
}
public static LazyDoubleCheckSingleton getInstance() {
if (null == instance) {
synchronized (LazyDoubleCheckSingleton.class) {
if (null == instance) {
instance = new LazyDoubleCheckSingleton();
}
}
} else {
/**
* 如果第一个线程 new LazyDoubleCheckSingleton() 的时候发生了指令重排序,---------------- 承上
* 第二个线程执行到这里(instance 不为空)时,构造函数中的代码可能还没有执行完,
* 那么使用 instance 变量是不安全的。
*/
}
return instance;
}
public static void main(String[] args) {
LazyDoubleCheckSingleton.getInstance();
}
}
什么是指令重排序?
机器级的优化操作,CPU 允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。
如果两个操作之间的关系不符合下列规则,就没有顺序性保障;虚拟机可以对这两个操作随意地进行重排序。
-
程序次序规则:在一个线程内,按照控制流顺序,书写在前面的代码先于后面的执行。
-
管程锁定规则:一个 unlock 操作先行发生于后面(时间先后)对同一个锁的 lock 操作。
-
volatile变量规则:对 volatile 变量的写操作先行发生于后面(时间先后)对这个变量的读操作。
-
线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每一个动作。
-
线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测【Thread::join()、Thread::isAlive()】。
-
线程中断规则:对线程的 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生【Thread::interrupted()、Thread::isInterrupted()】。
-
对象终结规则:一个对象的初始化完成(构造函数执行完成)先行发生于它的 finalize() 方法的开始。
-
传递性:如果操作A 先于操作B 发生,操作B 先于操作C 发生,那么操作A 先于操作C 发生。
Java 内存模型
想知道 volatile 是如何保证变量对所有线程可见 & 如何禁止指令重排序的,先了解 Java 内存模型,下面一张图告诉你答案: