深入理解Java内存模型:共享变量的可见性和有序性

🎣 开篇故事:消失的鱼罐头
工厂生产线要生产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()方法的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值