1 Java 内存模型
JMM 即 Java Memory Model, 他定义了主存,工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
- 原子性 - 保证指令不会收到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
2 可见性
2.1 问题
线程并不会因为 run 变量 更改为 false 而停下
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run) {
// 工作
}
}).start();
TimeUnit.SECONDS.sleep(5);
run = false;
}
分析:
(1)初始状态,t 线程刚开始从主内存读取了 run 变量的值 到 工作内存
(2) 因为 t 线程要频繁从主内存中读取 run 的值,JIT编译器会将 run 的值缓存至自己工作内存中 的高速缓存中,减少主存中 run 的访问,提高效率
(3)5秒之后,main线程修改了 run 的值,并同步至主存,而 t 线程是从自己工作内存中的高速缓存中读取整个变量的值,结果永远是旧值
2.2 解决
run 变量用 volatile 修饰
加上 volatile ,表示当前变量只能从主存中读取
static volatile boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run) {
// 工作
}
}).start();
TimeUnit.SECONDS.sleep(5);
run = false;
}
2.3 注意
- volatile 只能保证元素的可见性,不能保证元素的原子性,简单来说就是不能保证读取到的元素是最新的
- synchronized 语句块可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是 synchronized 是重量级锁操作,性能相对更低
3 有序性
3.1 概念
JVM会在不影响正确性的前提下,可以调整语句的执行顺序
static int i;
static int j;
// 在某个线程内执行以下赋值操作
i = ...;
j = ...;
可以看到,至于先执行 i 还是先执行 j,对最终的结果不会产生影响。所以,上面代码真正执行时,即可以是
i = ...;
j = ...;
又可以是
j = ...;
i = ...;
这种特性被称之为 指令重排,多线程下 指令重排 会影响正确性。
3.2 原理
现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。
指令可以划分为一个个更小的阶段
例如:每条指令可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 五个阶段
术语参考:
- instruction fetch (IF)
- instruction decode (ID)
- execute (EX)
- memory access (MEM)
- register write back (WB)
现代CPU都支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回,就可以称之为 五级指令流水线。这时CPU可以在一个时钟周期内同时运行五条指令的不同阶段。本质上,流水线技术并不能缩短单条指令的执行时间,但它变相的提高了指令地吞吐率。
3.3 问题
static int num = 0;
static int result = 0;
// ----------------------------------------------
new Thread(() -> {
num = 2;
ready = true;
}).start();
new Thread(() -> {
if (ready) {
result = 2 * num;
} else {
result = 1;
}
}).start();
result 因为指令重排 可能结果为 0
num = 2;
ready = true;
3.4 解决
static volatile boolean ready = false;
4 volatile 原理
volatile 的底层实现原理是内存屏障, Memory Barrier (Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
4.1 保证可见性
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
num = 2;
ready = true; // ready 是 volatile 赋值写屏障
// 写屏障
读屏障(Ifence)保证在该屏障之后,对共享变量的读取,加载的都是主存中最新数据
// 读屏障
// ready 是 volatile 读取值带读屏障
if (ready) {
result = 2 * num;
} else {
result = 1;
}
4.2 保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
num = 2;
ready = true; // ready 是 volatile 赋值写屏障
// 写屏障
读屏障会确保指令排序时,不会将读屏障之后的代码排在读屏障之前
// 读屏障
// ready 是 volatile 读取值带读屏障
if (ready) {
result = 2 * num;
} else {
result = 1;
}
4.3 指令交错
- 写屏障仅仅是保证之后的读能够督导最新的结果,但不能保证读在后面的情况
- 有序性的保证也只是保证了当前线程内的代码不被重排序
4.4 double-checked locking
- 多次检查 INSTANCE 是否为空是因为 只有第一次才需要对 Singleton 加锁保护,后面就不需要保护了
- volatile 修饰 INSTANCE 是为了保证 INSTANCE 的可见性,并且保证后续代码不被指令排序
- INSTANCE = new Singleton() 两步操作 创建对象 + 赋值引用
public final class Singleton {
private Singleton() {
// 构造方法
}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会金融业内部的 synchronized 代码块
if (INSTANCE == null) {
synchronized (Singleton.class) {
// 也许有其他线程已经创建实例,再判断一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
}
}