关注

JVM 核心 - Java内存模型(JMM)

1. 先搞懂:为什么需要 Java 内存模型(JMM)?

首先明确核心目的:JMM 是一套抽象的内存访问规范(不是实际的物理内存),解决多线程环境下的「可见性、原子性、有序性」问题,让 Java 程序在不同硬件/操作系统下有一致的内存访问行为

可以用一个比喻理解:

  • 每个线程就像一个“办公室员工”,有自己的“草稿纸”(线程私有工作内存);
  • 所有线程共享一个“公共文件柜”(主内存),存放共享变量;
  • 线程操作共享变量时,不能直接操作文件柜,必须先把变量拷贝到自己的草稿纸(工作内存),修改后再写回文件柜;
  • JMM 就是规定了“草稿纸和文件柜之间的数据交互规则”,避免多个员工同时改文件导致数据混乱。

2. JMM 的核心概念与结构

JMM 定义了线程、工作内存、主内存三者的交互关系:

  • 主内存:存储所有共享变量(实例变量、静态变量),是所有线程可见的公共区域;
  • 工作内存:每个线程独有的内存区域,存储共享变量的副本,线程对变量的所有操作(读/写)都必须在工作内存中进行,不能直接操作主内存;
  • 交互规则:JMM 定义了 8 种原子操作(lock/unlock、read/load、use/assign、store/write),规范了工作内存和主内存之间的数据传递流程(比如 read 是把主内存变量读到工作内存,write 是把工作内存变量写回主内存)。

3. JMM 解决的三大核心问题

这是 JMM 的核心价值,也是多线程并发的核心痛点:

(1)可见性:一个线程修改的变量,其他线程能立刻看到

问题场景:线程 A 修改了共享变量 flag,但只存在于 A 的工作内存,线程 B 还在读取自己工作内存中旧的 flag 值,导致逻辑错误。

// 可见性问题示例(不加 volatile 会导致线程 B 一直循环)
public class VisibilityDemo {
    private static boolean flag = false; // 共享变量,无 volatile
    
    public static void main(String[] args) throws InterruptedException {
        // 线程 B:循环读取 flag
        new Thread(() -> {
            while (!flag) {
                // 空循环
            }
            System.out.println("线程 B 退出循环");
        }).start();
        
        Thread.sleep(1000);
        
        // 线程 A:修改 flag 为 true
        new Thread(() -> {
            flag = true;
            System.out.println("线程 A 修改 flag 为 true");
        }).start();
    }
}

问题原因:线程 A 修改 flag 后,未及时写回主内存;即使写回,线程 B 也未从主内存重新读取,仍用工作内存的旧值。

JMM 的解决方案

  • volatile 关键字:强制变量的读写直接操作主内存,禁止工作内存缓存,保证修改对其他线程立即可见;
  • synchronized/Lock:加锁时会清空工作内存的旧值,解锁时会把工作内存的修改写回主内存,间接保证可见性。

修复后的代码

private static volatile boolean flag = false; // 加 volatile 保证可见性
(2)原子性:操作要么全部执行,要么全部不执行

问题场景:多线程对 count 进行自增(count++),该操作拆分为「读-改-写」三步,非原子操作,会导致最终结果小于预期。

// 原子性问题示例
public class AtomicityDemo {
    private static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        // 1000 个线程,每个线程执行 1000 次 count++
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    count++;
                }
            }).start();
        }
        
        Thread.sleep(2000);
        System.out.println("最终 count 值:" + count); // 结果远小于 1000000
    }
}

问题原因:线程 A 读取 count=10,还没修改,线程 B 也读取 count=10,两者都加 1 后写回,最终 count=11(本该是 12)。

JMM 的解决方案

  • synchronized/Lock:保证同一时间只有一个线程执行原子操作;
  • java.util.concurrent.atomic 包(AtomicInteger/AtomicLong):基于 CAS 操作实现原子性,效率更高。

修复后的代码

private static AtomicInteger count = new AtomicInteger(0);

// 替换 count++ 为原子操作
count.incrementAndGet();
(3)有序性:程序执行顺序与代码书写顺序一致

问题场景:JVM 为了优化性能,会对指令进行「重排序」(编译器重排序、CPU 重排序),单线程下不影响结果,但多线程下会导致逻辑混乱。

// 有序性问题示例(指令重排序导致线程 B 可能读到 a=0, b=1)
public class OrderingDemo {
    private static int a = 0;
    private static boolean b = false;
    
    public static void main(String[] args) throws InterruptedException {
        // 线程 A:先修改变量,再改标志位
        new Thread(() -> {
            a = 1;          // 指令1
            b = true;       // 指令2(可能被重排序到指令1 之前)
        }).start();
        
        // 线程 B:根据标志位读取变量
        new Thread(() -> {
            if (b) {        // 指令3
                System.out.println("a 的值:" + a); // 可能输出 0
            }
        }).start();
    }
}

问题原因:JVM 可能将线程 A 的指令重排序为「先执行 b=true,再执行 a=1」,导致线程 B 读到 b=truea=0

JMM 的解决方案

  • volatile 关键字:禁止指令重排序(除了保证可见性,还能保证有序性);
  • synchronized/Lock:保证同一时刻只有一个线程执行代码块,间接禁止重排序;
  • final 关键字:被 final 修饰的变量,初始化完成后不会被重排序。

修复后的代码

private static volatile int a = 0;
private static volatile boolean b = false;

4. JMM 的核心规则:happens-before(先行发生)

JMM 用「happens-before」规则定义操作之间的先后顺序,只要满足该规则,就保证可见性和有序性,无需额外同步。核心规则包括:

  1. 程序次序规则:单线程内,前面的操作 happens-before 后面的操作;
  2. volatile 变量规则:对 volatile 变量的写操作 happens-before 后续对该变量的读操作;
  3. 锁规则:解锁操作 happens-before 后续对同一把锁的加锁操作;
  4. 线程启动规则:Thread.start() 操作 happens-before 线程内的所有操作;
  5. 线程终止规则:线程内的所有操作 happens-before 线程的终止检测(如 Thread.join())。

举个例子:

volatile int x = 0;
// 线程 A
x = 1; // 写操作
// 线程 B
int y = x; // 读操作

根据「volatile 变量规则」,线程 A 的写操作 happens-before 线程 B 的读操作,因此 B 能读到 x=1,保证了可见性和有序性。

总结

  1. JMM 是抽象规范,核心解决多线程的「可见性、原子性、有序性」问题,定义了线程工作内存和主内存的交互规则;
  2. 关键解决方案:
    • 可见性:volatilesynchronizedLock
    • 原子性:synchronizedLock、Atomic 原子类;
    • 有序性:volatilesynchronizedLockfinal
  3. happens-before 规则是 JMM 的核心,满足该规则的操作无需额外同步,就能保证内存可见性和有序性。

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/u011614717/article/details/158495943

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--