深入理解Java高并发编程(4) - Java内存模型
1. JMM介绍
JMM是Java内存模型,定义了主存(线程共享的内存),工作内存(线程私有的变量)抽象概念,底层对应着CPU寄存器,缓存,硬件内存,CPU指令优化等。
JMM体现在以下几方面:
- 原子性:保证指令不会受到线程上下文切换影响
- 可见性:保证指令不会受cpu缓存的影响
- 有序性:保证指令不会收cpu指令并行优化的影响
2. 可见性问题
JIT编译器会把多次从主存中读取的值缓存到了线程私有的工作内存中,当另一个线程对主存中的变量进行修改,该线程仍然读的是工作内存中的数据而产生的问题,这就是可见性问题。
3. 有序性问题
3.1 什么是指令重排序?
JVM会对指令的执行顺序进行重排,在底层的cpu有多种执行单元,不同的执行单元执行不同的事情,比如取指令,指令译码,执行指令,内存访问,数据写回等步骤,如果cpu还按照这个顺序一条指令一条指令的执行,有些执行单元就会闲置,当指令重排之后,就能保证cpu执行这条指令的取指令时还能执行另一条指令的指令译码。
比如同时支持取指令,指令译码,执行指令,内存访问,数据写回的处理器就被称为五级指令流水线。

当然指令也不是随便重排的,重排指令不能影响结果。
3.2 指令重排序带来的问题
单线程下指令重排序是没有问题的,在多线程场景下指令重排序可能会带来一些问题。
在多线程场景下,指令重排可能会导致一个线程的行为对另一个线程来说难以预测。打比方说线程1修改了一个共享变量,线程2想输出这个共享变量,指令重排就可能导致线程2在修改前输出这个共享变量,也可能在修改后输出共享变量。
4. synchronize对于原子性,可见性,有序性
- 原子性:
- synchronized通过给对象加锁,保证临界区代码只有一个线程能访问从而保证了原子性
- 可见性:
- 锁释放(Unlock)时: 线程会强制将工作内存(CPU缓存)中修改的共享变量刷新回主内存。
- 锁获取(Lock)时: 线程会强制将工作内存中的共享变量置为无效,这样后续使用该变量时,就必须从主内存中重新加载最新的值。
- 有序性:
synchronized的有序性保证和volatile有些类似,也是通过内存屏障来实现的。- 加锁(MonitorEnter):会插入
LoadLoad + LoadStore屏障,屏障之后的所有读写操作,不能重排序到屏障之前。 - 释放锁(MonitorExit):会插入
StoreStore + StoreLoad屏障,屏障之前的所有读写操作,不能重排序到屏障之后。
- 加锁(MonitorEnter):会插入
| 内存屏障 | 结构示意 | 核心规则(禁止重排) | 通俗理解 |
|---|---|---|---|
| LoadLoad | Load1 → LoadLoad → Load2 | 后面的读,不能重排到前面的读之前 | 先读前面,再读后面,读读有序 |
| StoreStore | Store1 → StoreStore → Store2 | 前面的写,不能重排到后面的写之后 | 先写前面,再写后面,写写有序 |
| LoadStore | Load → LoadStore → Store | 后面的写,不能重排到前面的读之前 | 先读完,再写入,读写有序 |
| StoreLoad | Store → StoreLoad → Load | 前面的写全部刷主存后,后面的读才能执行 | 写完刷主存,再开始读,写读有序 |
5. Volatile关键字
volatile关键字修饰的变量要求线程不能从工作缓存中读取变量,而是每次都从主存中去读取变量,线程操作Volatile变量都是直接操作主存,这样虽然有效率损失,但是能保证可见性。
Volatile可以保证可见性和有序性,但是不能保证原子性,因为他仅仅是操作了主存,没有保证一系列的指令是原子的
5.1 Volatile 如何保证可见性和有序性
Volatile通过读写屏障来保证可见性和有序性;
-
对volatile变量的赋值(写操作)会产生写屏障
LoadStore + StoreStore -
对volatile变量的读取(读操作)会产生读屏障
LoadLoad + LoadStore -
可见性:
- 写屏障会保证在写屏障之前的所有对变量的改动,都同步到主存当中。
- 读屏障保证在读屏障之后的所有对共享变量的读取,都会从主存中读取。
-
有序性
- 写屏障会保证写屏障之前的指令不会被指令重排到写屏障之后去,但是写屏障之前的代码还是会指令重排的。
- 读屏障保证读屏障之后的指令不会被指令重排到读屏障之前去,但是在读屏障之后的代码还是会指令重排的。
6. DCL问题(double-checked locking)
6.1 懒汉式单例模式
对于懒汉式单例模式,单例一开始不会被初始化,直到调用getInstance()时,才会开始创建实例
6.2 DCL解决多线程下单例创建
懒汉式单例创建时属于共享变量,存在线程安全问题。
采用DCL的机制可以避免单例被多次创建
public final class Singleton {
private Singleton() {}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
//第一次操作共享变量也要保证原子性,有序性,可见性
if(INSTANCE == null) {
//synchronized保证没有原子性,有序性,可见性问题,但是synchronized临界区内仍然可能发生指令重排
synchronized(Singleton.class) {
//double check
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
7. happens-before规则
遵循happens-before规则,能保证一个线程的写,对于另一个线程的读是可见的
-
线程解锁m前对变量的写,对于接下来加锁m的线程的读是可见的
-
对volatile变量的写,对其他线程对该变量的读是可见的
-
线程start前对变量的写,对线程开始后对该变量的读可见(线程刚启动会从主存读变量到工作内存)
-
线程结束对变量的写,在其他线程得知它结束的读可见(线程结束前会将工作内存变量写入主存)
-
线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见(也就是t1,打断t2,主线程去看t2的打断标记,也能看到之前t2的修改)
-
对默认值的写,对其他线程对该变量读可见
-
传递性: x hb-> y, y hb- > z ,x hb-> z

浙公网安备 33010602011771号