Java内存模型

多线程编程的混乱

现代计算机CPU有多级缓存,每个核心有自己的缓存,且编译器/处理器会进行指令重排序优化。这导致:

  • 可见性问题:线程A修改了共享变量,但修改后的值可能只写回了自己的CPU缓存,没有及时同步到主内存,线程B因此看不到最新值。
  • 有序性问题:代码的编写顺序不等于最终的执行顺序,可能导致意想不到的结果。

JMM

这就需要Java内存模型了,JMM 主要定义了对于一个共享变量,当一个线程执行写操作后,该变量对其他线程的可见性

它不是指Java程序运行时真实的物理内存结构(如堆、栈等),而是一套抽象的规范。这套规范定义了:

  • Java程序中各种变量(线程共享变量)的访问规则
  • 以及在JVM中,将变量存储到内存从内存读取变量的底层细节。

其存在目的是解决在多线程并发环境下,由于CPU缓存、指令重排序等问题导致的内存可见性、原子性、有序性 问题,为多线程编程提供一个一致的、可预测的内存访问视图

JMM如何抽象线程和主内存之间的关系

JMM通过一个简化的三层抽象模型来定义线程、工作内存和主内存之间的关系:

  • 主内存(Main Memory)

    • 存储所有共享变量(实例字段、静态字段、数组元素)
    • 是线程间共享的内存区域
    • 相当于硬件内存的抽象
  • 工作内存(Working Memory)

    • 每个线程私有的存储区域
    • 存储该线程使用的变量的主内存副本拷贝
    • 包含栈帧中的局部变量表、操作数栈、动态链接、方法出口等信息
    • 相当于CPU寄存器+高速缓存的抽象
  • 线程(Thread)

    • 程序执行的最小单位
    • 所有变量操作都发生在工作内存中
    • 不能直接读写主内存中的变量

JMM定义了8种原子操作来完成线程、工作内存、主内存之间的交互:

  1. lock(锁定) : 作用于主内存变量,将其标识为线程独占状态
  2. unlock(解锁) : 作用于主内存变量,释放锁定状态
  3. read(读取) : 作用于主内存,将变量值传输到线程工作内存
  4. load(载入) : 作用于工作内存,将read得到的值放入变量副本
  5. use(使用) : 作用于工作内存,将变量值传递给执行引擎
  6. assign(赋值) : 作用于工作内存,将执行引擎接收的值赋给变量
  7. store(存储) : 作用于工作内存,将变量值传输到主内存
  8. write(写入) : 作用于主内存,将store传输的值放入变量

线程要读取变量x: [主内存] --read--> [数据传输] --load--> [工作内存] --use--> [执行引擎]

线程要修改变量x: [执行引擎] --assign--> [工作内存] --store--> [数据传输] --write--> [主内存]

案例1:工作内存与主内存的数据不一致的案例

arduino 复制代码
public class MemoryVisibilityDemo {
    private static boolean ready = false;
    private static int number = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread readerThread = new Thread(() -> {
            // 工作内存中:ready的初始值是false
            while (!ready) {
                // 空转,等待ready变为true
                // 问题:线程可能永远读取不到主内存中最新的ready值
            }
            // 如果看到ready为true,打印number
            System.out.println("Number: " + number);
        });
        
        Thread writerThread = new Thread(() -> {
            // 在工作内存中修改number
            number = 42;
            // 在工作内存中修改ready
            ready = true;
            // 注意:此时修改可能还停留在工作内存,没有刷新到主内存
        });
        
        readerThread.start();
        writerThread.start();
        readerThread.join();
        writerThread.join();
    }
}

以上代码的执行流程: 时间线:

  1. writerThread启动:

    • 从主内存read-load number副本到工作内存
    • assign number=42到工作内存副本
    • 从主内存read-load ready副本到工作内存
    • assign ready=true到工作内存副本
    • (没有立即store-write回主内存!)
  2. readerThread启动:

    • 从主内存read-load ready副本到工作内存(此时主内存中ready=false)
    • 循环检查工作内存中的ready副本(始终为false)
    • 永远无法看到writerThread的修改!

结果:可能死循环,也可能正常结束(依赖具体JVM实现和硬件)

案例2:使用volatile改进

arduino 复制代码
public class VolatileVisibilityDemo {
    // 使用volatile修饰,保证可见性
    private static volatile boolean ready = false;
    private static int number = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread readerThread = new Thread(() -> {
            while (!ready) {
                // 由于ready是volatile,每次循环都会从主内存重新读取
            }
            System.out.println("Number: " + number);
        });
        
        Thread writerThread = new Thread(() -> {
            number = 42;
            // volatile写操作:1. 立即将工作内存的值刷新到主内存
            //                2. 使其他CPU缓存中该变量的缓存行无效
            ready = true;
        });
        
        readerThread.start();
        Thread.sleep(100); // 确保reader先运行
        writerThread.start();
        
        readerThread.join();
        writerThread.join();
    }
}

以上代码的执行过程: writerThread的volatile写操作:

  1. assign ready=true (工作内存)
  2. store ready (工作内存 -> 传输)
  3. write ready (传输 -> 主内存) [立即执行,插入StoreStore内存屏障]
  4. 发送缓存失效信号给其他CPU

readerThread的volatile读操作:

  1. 发现本地缓存失效
  2. read ready (主内存 -> 传输)
  3. load ready (传输 -> 工作内存) [插入LoadLoad内存屏障]
  4. use ready (工作内存 -> 执行引擎)

案例3:synchronized的完整内存语义

typescript 复制代码
public class SynchronizedMemoryDemo {
    private int sharedValue = 0;
    private final Object lock = new Object();
    
    public void writer() {
        synchronized(lock) {
            // 1. 进入同步块:清空工作内存,从主内存重新加载所有共享变量
            sharedValue = 100;
            // 2. 修改只发生在工作内存
        }
        // 3. 退出同步块:将工作内存的修改刷新到主内存
    }
    
    public void reader() {
        synchronized(lock) {
            // 4. 进入同步块:清空工作内存,从主内存重新加载所有共享变量
            System.out.println("Value: " + sharedValue); // 保证看到最新值
        }
    }
    
    public static void main(String[] args) {
        SynchronizedMemoryDemo demo = new SynchronizedMemoryDemo();
        
        Thread t1 = new Thread(demo::writer);
        Thread t2 = new Thread(demo::reader);
        
        t1.start();
        t2.start();
    }
}

上述代码的执行过程:

monitorenter(获取锁):

  1. 清空工作内存中所有共享变量的副本
  2. 从主内存重新加载所有需要的共享变量
  3. 执行同步块内的代码

同步块内执行:

  • 所有操作都在工作内存中进行
  • 修改不会立即写回主内存

monitorexit(释放锁):

  1. 将工作内存中修改的所有共享变量刷新到主内存
  2. 释放锁

内存屏障(Memory Barrier)

内存屏障,也可以称为内存栅栏(Memory Fence),它是在程序中对内存访问操作(读/写)施加的一种特殊约束。

核心作用

  • 保证特定操作的顺序 :确保屏障前的某些操作必须在屏障后的某些操作之前完成。
  • 保证内存的可见性:确保一个线程对共享变量的修改能立即对其他线程可见。

JMM通过在适当位置插入内存屏障来保证可见性和有序性:

csharp 复制代码
public class MemoryBarrierExample {
    private int x = 0;
    private volatile boolean v = false;
    
    public void writer() {
        x = 1;          // 普通写
        // StoreStore屏障:确保x=1先于v=true刷新到主内存
        v = true;       // volatile写(隐含内存屏障)
    }
    
    public void reader() {
        if (v) {        // volatile读(隐含内存屏障)
            // LoadLoad屏障:确保读取x之前,v的读取已完成
            int r = x;  // 看到x=1
        }
    }
}

happends-before原则

Happens-Before 原则 是 Java 内存模型(JMM)中定义的一组跨线程操作间的可见性保证规则 。它规定了在什么情况下,一个线程对共享变量的修改结果必须对另一个线程可见。

如果一个操作 A happens-before 操作 B,那么:

  1. A 操作对内存的影响(如写入的变量值)在 B 操作执行时是可见的
  2. 编译器和处理器可以对指令重排序,但必须遵守 happens-before 规则

它是为了解决编译器和处理器会进行指令重排序优化的问题

问题示例:

csharp 复制代码
// 问题示例
public class VisibilityProblem {
    private boolean flag = false;
    private int value = 0;
    
    public void writer() {
        value = 42;       // 操作1
        flag = true;      // 操作2 ← 可能被重排序到操作1之前
    }
    
    public void reader() {
        if (flag) {       // 可能看到true,但value仍为0!
            System.out.println(value);
        }
    }
}

没有 happens-before 时的问题

  • 编译器/CPU 可能重排序操作1和操作2
  • 线程B可能看到 flag=truevalue=0
  • 不同线程对操作执行顺序可能有不同视角

8条happens-before原则

规则1:程序顺序规则(Program Order Rule)

在同一个线程中,按照程序代码顺序,前面的操作 happens-before 后面的操作。

arduino 复制代码
public class ProgramOrderRule {
    int x = 0;
    boolean ready = false;
    
    // 线程A执行
    public void writer() {
        x = 1;           // 操作1
        ready = true;    // 操作2
        // 在同一个线程内:操作1 happens-before 操作2
        // 单线程视角下,操作顺序固定
    }
}

规则2:监视器锁规则(Monitor Lock Rule)

对一个锁的解锁 happens-before 随后对这个锁的加锁。

typescript 复制代码
public class MonitorLockRule {
    private final Object lock = new Object();
    private int sharedData = 0;
    
    public void writer() {
        synchronized(lock) {      // 加锁
            sharedData = 42;      // 操作1
        }                         // 解锁
        // 解锁 happens-before 后续对这个锁的加锁
    }
    
    public void reader() {
        synchronized(lock) {      // 加锁(看到writer的解锁)
            System.out.println(sharedData);  // 保证看到42
        }
    }
}

规则3:volatile变量规则(Volatile Variable Rule)

对一个 volatile 变量的写操作 happens-before 后续对这个变量的读操作。

csharp 复制代码
public class VolatileRule {
    private volatile boolean flag = false;
    private int data = 0;
    
    public void writer() {
        data = 42;           // 操作1
        flag = true;         // 操作2(volatile写)
        // 操作1 happens-before 操作2(程序顺序)
        // volatile写 happens-before 后续的volatile读
    }
    
    public void reader() {
        if (flag) {          // volatile读
            // 保证能看到 data = 42
            System.out.println(data);
        }
    }
}

规则4:线程启动规则(Thread Start Rule)

线程的 start() 方法调用 happens-before 该线程的任何操作。

arduino 复制代码
public class ThreadStartRule {
    private int data = 0;
    
    public void test() {
        data = 100;
        
        Thread thread = new Thread(() -> {
            // 这里能看到 data = 100
            System.out.println("Thread sees data: " + data);
        });
        
        // main线程中data的赋值 happens-before thread.start()
        thread.start();
    }
}

规则5:线程终止规则(Thread Termination Rule)

线程中的所有操作 happens-before 其他线程检测到该线程已经终止(如 join() 返回)。

arduino 复制代码
public class ThreadJoinRule {
    private int result = 0;
    
    public void test() throws InterruptedException {
        Thread thread = new Thread(() -> {
            result = 42;  // 线程中的操作
        });
        
        thread.start();
        thread.join();    // 等待线程结束
        
        // 线程中的所有操作 happens-before join()返回
        System.out.println(result);  // 保证看到42
    }
}

规则6:线程中断规则(Thread Interrupt Rule)

对线程 interrupt() 的调用 happens-before 被中断线程检测到中断。

arduino 复制代码
public class InterruptRule {
    public void test() {
        Thread thread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                // 工作
            }
            // 能感知到中断状态
        });
        
        thread.start();
        thread.interrupt();  // happens-before 线程检测到中断
    }
}

规则7:对象终结规则(Finalizer Rule)

对象的构造方法结束 happens-before finalize() 方法的开始。

typescript 复制代码
public class FinalizerRule {
    private final Object resource;
    
    public FinalizerRule() {
        this.resource = new Object();
        // 构造函数中的操作 happens-before finalize()
    }
    
    @Override
    protected void finalize() {
        // 这里能访问构造函数初始化的resource
        cleanup(resource);
    }
}

规则8:传递性规则(Transitivity Rule)

如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

Happens-Before 原则的本质 :Java 内存模型为程序员提供的一组最小保证,在这些保证下,即使存在指令重排序和内存可见性问题,程序的行为也是可预测的。

相关推荐
暮色妖娆丶9 小时前
不过是吃了几年互联网红利罢了,我高估了自己
java·后端·面试
NE_STOP9 小时前
MyBatis-参数处理与查询结果映射
java
狂奔小菜鸡10 小时前
Day40 | Java中的ReadWriteLock读写锁
java·后端·java ee
SimonKing11 小时前
JetBrains 用户狂喜!这个 AI 插件让 IDE 原地进化成「智能编码助手」
java·后端·程序员
狂奔小菜鸡11 小时前
Day39 | Java中更灵活的锁ReentrantLock
java·后端·java ee
NE_STOP1 天前
MyBatis-配置文件解读及MyBatis为何不用编写Mapper接口的实现类
java
后端AI实验室1 天前
用AI写代码,我差点把漏洞发上线:血泪总结的10个教训
java·ai
程序员清风1 天前
小红书二面:Spring Boot的单例模式是如何实现的?
java·后端·面试