
00:00:00
外卖卷:非常划算 扫码领劵 省点小钱钱
Java 内存模型(JMM)是一个抽象概念,它不是实际存在的硬件或软件,而是一套规则和规范。这套规则定义了程序中所有变量(包括实例变量、静态变量和数组元素)在内存中的访问方式,特别是在多线程环境下的行为。
JMM 的作用:
核心思想:

主内存与工作内存的对应关系(与 JVM 内存区域的区别):JMM 的主内存和工作内存是抽象概念,与 JVM 运行时数据区(堆、栈、方法区等)不是直接对应的。
JMM 定义了 8 种原子操作来规范主内存和工作内存之间的数据交互。每个操作都是不可中断的。
注意:除了
volatile修饰的long和double类型变量,其他long和double变量的读写操作在 32 位系统上可能不是原子的,而是被拆分成两次 32 位操作。

lock(锁定):作用于主内存,将变量标记为线程独占状态。unlock(解锁):作用于主内存,释放变量的独占状态,允许其他线程锁定。read(读取):作用于主内存,将变量值从主内存传输到线程的工作内存。load(载入):作用于工作内存,紧接在 read 之后,将 read 到的值放入工作内存的变量副本。use(使用):作用于工作内存,将工作内存中变量的值传递给执行引擎(CPU),每次使用变量前都需要。assign(赋值):作用于工作内存,将执行引擎计算出的值赋给工作内存的变量。store(存储):作用于工作内存,将工作内存中变量的值传输到主内存。write(写入):作用于主内存,紧接在 store 之后,将 store 得到的值写入主内存的变量。规则总结:这些操作必须遵循严格的顺序和组合规则,以确保内存操作的正确性。例如,
read和load必须成对出现,store和write也必须成对出现。lock和unlock必须成对,且unlock前必须先store和write。lock操作会清空工作内存中该变量的副本,强制线程重新从主内存加载最新值。
参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java 并发.md
定义:当多个线程访问同一个共享变量时,一个线程修改了这个变量的值,其他线程能够立即看到并读取到这个修改后的最新值。
问题根源:由于每个线程都有自己的工作内存(缓存),它们操作的是共享变量的副本。如果一个线程修改了副本,但没有及时同步回主内存,或者其他线程没有及时从主内存刷新副本,就会导致“不可见”问题,即线程读取到的是旧值。
示例:main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止
static boolean run = true; // 默认值是 true
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(run) { // t 线程持续检查 run 变量
// ... 其他操作
}
System.out.println("t 线程停止了。");
});
t.start();
Thread.sleep(1000); // 主线程等待 1 秒
run = false; // 主线程修改 run 的值
System.out.println("主线程将 run 设置为 false。");
// 预期:t 线程应该在 1 秒后停止,但实际可能不会。
}原因分析:
t 线程启动后,将 run 变量的初始值 true 从主内存读取到自己的工作内存中。while(run) 循环优化为直接读取 t 线程工作内存中的 run 副本,甚至可能将其视为一个常量 true,从而减少频繁访问主内存。main 线程修改了 run 的值为 false,并同步回主内存。t 线程的盲区:由于 t 线程可能持续从自己的工作内存缓存中读取 run 的旧值(true),它无法感知 main 线程对主内存中 run 变量的修改,导致 while(run) 循环永远不会结束。
解决方案:
volatile 关键字修饰 run 变量:volatile 强制所有线程从主内存读取最新值,并立即写回主内存。synchronized 或 Lock:它们不仅保证原子性,也隐式地保证了可见性(在释放锁前会把工作内存修改同步回主内存,在获取锁后会清空工作内存,强制从主内存读取)。final 关键字修饰的变量是不可变的,虽然不是直接解决可见性问题,但其不可变性天然保证了多线程环境下的一致性(因为值不会变)。定义:一个或一系列操作是不可分割的,要么全部成功执行,要么全部不执行(失败),在执行过程中不会被任何其他线程的操作打断。它保证了指令的完整性,不会受到线程上下文切换的影响。
JMM 对原子操作的规则:JMM 定义了 8 个内存交互操作,这些操作本身是原子的。但为了保证更高级别操作的原子性,JMM 规定了一些使用规则:
read 和 load 必须成对出现,不能单独执行。store 和 write 也必须成对出现。assign 操作,即对工作内存变量的修改必须同步回主内存。assign 操作)。assign 或 load 操作(即从主内存初始化)。lock 操作。lock 操作可以被同一线程重复执行多次(可重入),但只有执行相同次数的 unlock 操作后,变量才会被真正解锁。lock 和 unlock 必须成对出现。lock 操作会清空工作内存中此变量的值,强制线程在使用前重新从主内存加载。lock 操作锁定的变量执行 unlock 操作,也不允许解锁被其他线程锁定的变量。unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)。解决方案:
synchronized 关键字:保证代码块或方法的原子性。java.util.concurrent.atomic 包下的原子类:如 AtomicInteger、AtomicLong,它们通过 CAS 操作实现原子性。java.util.concurrent.locks.Lock 接口的实现类:如 ReentrantLock。定义:
指令重排序:为了提高性能,编译器和处理器会对指令进行优化,调整其执行顺序,只要不改变单线程内的执行结果,这种重排序就是允许的。
重排序的阶段:源代码 → 编译器优化的重排 → 指令并行的重排 → 内存系统的重排 → 最终执行指令
示例:著名的“双重检查锁定(DCL)”单例模式中,如果不使用 volatile,就可能因为指令重排序导致问题。
class Singleton {
private static Singleton instance; // 考虑不使用 volatile 的情况
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
// 这行代码可能发生指令重排序
// 1. memory = allocate() 分配内存
// 2. ctorInstance(memory) 初始化对象
// 3. instance = memory 设置 instance 指向内存
// 如果重排序为 1 -> 3 -> 2,那么在 3 执行后,另一个线程可能看到非空的 instance,
// 但此时对象还未完全初始化,导致访问到半初始化对象。
instance = new Singleton();
}
}
}
return instance;
}
}解决方案:使用 volatile 关键字:volatile 除了保证可见性,还能禁止指令重排序(特别是针对 volatile 变量的读写操作)。在 DCL 中,private static volatile Singleton instance; 可以解决重排序问题。
评论