🎣 开篇故事:消失的鱼罐头
工厂生产线要生产100个鱼罐头,流程是:
1. 装鱼 → 2. 封盖 → 3. 贴标签
但工人们发现:有时罐头还没装鱼就贴了标签!
这就是代码世界的 “指令重排序” 问题!
为什么要有Java内存模型?
Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果。
JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
🧠 内存三难题(新手必懂的三个核心问题
- 原子性-保证指令不会受到线程上下文切换的影响,如转账时ATM故障,钱扣了但没到账
- 可见性-保证指令不会受cpu缓存的影响,如妈妈喊你吃饭,你戴着耳机没听见
- 有序性-保证指令不会受cpu指令并行优化的影响,如先贴标签后装鱼的错误罐头
1.可见性
🔍 可见性破案:永远循环的线程
@Slf4j
public class Test1 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (run) {
//
}
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
run = false;
log.info("run stop");
}
}
为什么停不下来?
👂 线程有自己的“耳朵”(工作内存)
📻 需要volatile大喇叭广播主内存的变化!
工作流程
1.初始状态,t 线程刚开始从主内存读取了run的值到工作内存。
2.因为 t线程要频繁从主内存中读取run的值,JITI 编译器 会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率
3.1秒之后,main 线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方法
- 共享变量加volatile关键字
- 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低
2. 有序性
JVM可以在不影响程序正确性的前提下调整指令的执行顺序,这种特性称为指令重排。在多线程环境中,指令重排可能导致错误的结果。
内存屏障:是一种屏障指令,它使得CPU或编译器对屏障指令的 前 和 后 所发出的内存操作执行一个排序的约束 。也叫内存栅栏或栅栏指令
内存屏障能干嘛?
- 阻止屏障两边的指令重排序
- 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
- 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新的数据
volatile的实现原理
volatile的底层实现原理是内存屏障,Memory Barrier (Memory Fence)。它通过以下方式保证可见性和有序性:
- 对 volatile 变量的写操作后会加入写屏障,确保之前的操作在写入主存之前完成。
- 对 volatile 变量的读操作前会加入读屏障,确保读取的是主存中的最新数据。
如何保证可见性?
可见性是指保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程都可见。
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是volatile 赋值带写屏障
//写屏障
}
- 而读屏障(lfence) 保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_ Result r) {
//读屏障
// ready是volatile 读取值带读屏障
if(ready) {
} else{
}
}
如何保证有序性?
- volatile写之前的操作,都禁止重排序到volatile之后
- volatile读之后的操作,都禁止重排序到volatile 之前
- volatile写之后volatile读,禁止重排序
如何正确使用volatile?
- 适用于状态标志和简单的读写操作。
- 不适合用于复合操作(如自增等)。
先行发生(Happens-Before)原则
JMM本质上包含一些规则,其中就有Happens-Before原则。
Happens-Before 规则规定了对共享变量的写操作对其他线程的读操作可见。理解这些规则对于编写并发安全的程序至关重要。
主要规则:
- 次序规则:一个线程内,按照代码顺序执行的操作先行发生于后面的操作。
- 锁定规则:一个线程的 unlock 操作先行发生于后续对同一锁的 lock 操作。
- volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面对该变量的读操作,前面的写对后面的读是可见的。
- 传递规则:如果操作 A 先行发生于 B,且 B 先行发生于 C,则 A 先行发生于 C。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread interrupted()检测到是否发生中断,也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发生
线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行。
对象终结规则(FinalizerRule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。说人话:对象没有完成初始化之前,是不能调用finalized()方法的。