JMM 8种原子性内存操作全解析(附三大特性保障案例)

JMM定义的8种原子性内存操作是主内存与工作内存交互的唯一标准方式,所有线程对共享变量的读、改、同步行为,均由这8种操作组合而成;且每种操作本身不可拆分、原生原子,并通过严格的执行约束、操作组合,为JMM的原子性、可见性、有序性三大核心特性提供底层支撑。

首先明确8种操作的分类与核心关联:按作用区域可分为「主内存专属操作」(lock、unlock)和「主内存-工作内存交互操作」(read、load、use、assign、store、write),其中交互操作需成对执行才有效(如read必须搭配load,store必须搭配write),单独执行无任何语义。

下面先详细说明每种操作的作用区域、核心语义、执行约束,再分别拆解8种操作如何通过原生特性、组合规则、执行约束保障原子性、可见性、有序性,并结合实际场景和代码案例说明,让原理落地。

一、8种原子性内存操作详细说明

为便于理解,按「主内存专属操作」「主从交互操作」分类,明确每种操作的核心规则,这是理解三大特性保障的基础:

(一)主内存专属操作(2种):仅作用于主内存的共享变量,是并发同步的核心

操作名称 作用区域 核心语义 严格执行约束
lock(锁定) 主内存 对主内存中的某个共享变量加锁,标记为「当前线程独占」状态 1. 仅作用于共享变量,局部变量无锁可加;
2. 一个变量同一时间仅能被一个线程锁定
3. 线程可对已锁定的变量重复加锁(可重入),需对应次数的unlock解锁
unlock(解锁) 主内存 释放主内存中已被当前线程锁定的共享变量,取消独占标记 1. 仅能解锁当前线程锁定的变量,不能解锁其他线程的锁;
2. 解锁前必须完成store+write操作(强制将工作内存中修改后的变量副本写回主内存);
3. 未锁定的变量执行unlock会直接报错

(二)主内存-工作内存交互操作(6种):完成变量的「主存→工作内存」读取和「工作内存→主存」写回,需成对执行

操作名称 作用链路 核心语义 严格执行约束
read(读取) 主内存→工作内存 将主内存中共享变量的最新值传递到工作内存的临时缓冲区 1. 仅读取主内存的当前最新值
2. 必须与load成对执行,仅read不load,工作内存无法创建变量副本
load(加载) 工作内存内部 将read传递到临时缓冲区的变量值,加载到工作内存的共享变量副本中 1. 必须紧跟read操作;
2. 加载后,工作内存才有该变量的可操作副本
use(使用) 工作内存→执行引擎 将工作内存中共享变量副本的当前值传递给Java执行引擎(用于计算、判断等业务逻辑) 1. 执行引擎使用变量前必须先完成load/assign(确保副本有有效值);
2. 变量值被修改后(assign),需重新执行use才能传递新值
assign(赋值) 执行引擎→工作内存 将执行引擎的计算结果赋值给工作内存的共享变量副本,覆盖旧值 1. 仅当执行引擎产生新值时执行(如i++、flag=true);
2. 赋值后,工作内存的副本为最新修改值(未写回主内存前,主存值不变)
store(存储) 工作内存→主内存 将工作内存中修改后的共享变量副本值,传递到主内存的临时缓冲区 1. 仅传递assign修改后的新值
2. 必须与write成对执行,仅store不write,主内存的原变量值不会更新
write(写入) 主内存内部 将store传递到临时缓冲区的新值,写入主内存的原共享变量,更新主存的最新值 1. 必须紧跟store操作;
2. 写入前若变量被lock锁定,需等待解锁后再执行;
3. 禁止写入未被load/assign初始化的无效值

核心执行原则:这6种操作的执行需遵循「读取链路」和「写回链路」的固定顺序,不可跨链路乱序

  • 读取链路:read → load → use(线程使用共享变量的唯一顺序)
  • 写回链路:assign → store → write(线程修改共享变量后写回主存的唯一顺序)

二、8种原子操作如何保障「原子性」(附案例)

1. 原子性的底层保障原理

原子性的核心是「操作不可拆分、执行过程不被打断」,8种操作对原子性的保障分为两层,覆盖「基础内存操作」和「业务复合操作」,是解决多线程数据覆盖、丢失更新的核心:

  • 底层基础:8种操作自身均为原生原子操作,JMM强制每种操作的执行过程不可被任何线程打断,是原子性的基础支撑;
  • 复合保障:通过lock+unlock对「read-load-use-assign-store-write」操作序列进行独占包裹,让多个原子操作组成的复合业务操作整体成为原子操作(如i++、对象赋值)。

2. 关键操作支撑

  • 核心支撑操作:lockunlock(独占锁机制,是复合操作原子性的核心);
  • 基础支撑操作:readloaduseassignstorewrite(自身原子,为复合操作提供基础);
  • 核心约束:unlock的执行前提是「完成store+write」,且lock加锁后,其他线程无法对该变量执行任何内存操作,直到当前线程unlock解锁。

3. 代码案例:解决i++的原子性缺失问题

i++是典型的复合操作,底层对应「read→load→use(i+1)→assign→store→write」6个原子操作的组合,单线程下无问题,多线程下因操作序列可被打断,会出现数据丢失(如10线程各执行1000次i++,预期10000,实际远低于该值)。

(1)无原子性保障的问题场景(无lock/unlock包裹)

public class AtomicityTest {
    public static int i = 0; // 共享变量

    // 无原子性保障:i++的6步操作可被多线程打断
    public static void increment() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                for (int k = 0; k < 1000; k++) {
                    increment();
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("最终i的值:" + i); // 实际结果远小于10000,如8923、9567等
    }
}

问题根源:无lock+unlock包裹,线程A执行到use(i+1)时,线程B可同时执行read→load读取到相同的i值,最终两个线程都写回相同的新值,丢失一次自增。

(2)加原子性保障的解决场景(synchronized底层封装lock+unlock)

synchronized是JMM层面对lock+unlock的高层封装,修饰方法/代码块时,会自动对共享变量加lock,执行完代码后自动unlock,且unlock前强制完成store+write,让「read→load→use→assign→store→write」成为整体原子操作

public class AtomicityTest {
    public static int i = 0;

    // synchronized底层:lock→执行i++→store+write→unlock,整体原子
    public synchronized static void increment() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        // 同上述代码
        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                for (int k = 0; k < 1000; k++) {
                    increment();
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("最终i的值:" + i); // 稳定输出10000,保障原子性
    }
}

保障逻辑:线程A执行increment时,先对i执行lock加锁,其他线程无法对i执行任何内存操作;线程A完成i++的6步操作后,先执行store+write将新值写回主内存,再执行unlock解锁;此时其他线程才能获取锁并执行i++,确保整个操作序列不被打断。

三、8种原子操作如何保障「可见性」(附案例)

1. 可见性的底层保障原理

可见性的核心是「一个线程修改的共享变量,其他线程能及时感知并获取最新值」,8种操作通过三大强制约束实现该目标,核心是「修改必写回、读取必刷新、解锁必同步」,确保工作内存的修改能快速同步到主内存,其他线程的工作内存能及时从主内存加载最新值。

2. 关键操作支撑与核心约束

  • 核心支撑操作:assignstorewritereadloadunlock(前三者负责修改写回,中间二者负责读取刷新,最后一个负责强制同步);
  • 三大核心约束(JMM强制执行):
    1. 线程执行assign修改工作内存的副本后,若变量被volatile修饰/被lock锁定,必须立即执行store+write,将新值同步到主内存(禁止修改后缓存在工作内存);
    2. 线程执行use使用共享变量前,若变量被volatile修饰/被lock锁定,必须先执行read+load从主内存重新加载最新值(强制失效工作内存的旧副本,禁止使用缓存值);
    3. 线程执行unlock解锁前,必须完成store+write(JMM最严格的约束之一),确保锁释放前,所有修改都已同步到主内存,其他线程获取锁后能读取到最新值。

3. 代码案例:解决共享变量的可见性缺失问题

普通共享变量因无强制的store+write和read+load约束,线程修改后可能仅缓存在工作内存,未及时写回主内存,导致其他线程始终读取工作内存的旧副本,出现可见性问题;通过volatilesynchronized(底层封装操作约束)可强制触发上述规则,解决问题。

(1)无可见性保障的问题场景(普通共享变量)

public class VisibilityTest {
    public static boolean flag = false; // 普通共享变量,无约束

    public static void main(String[] args) throws InterruptedException {
        // 线程1:循环使用flag,无强制read+load,始终使用工作内存旧副本
        new Thread(() -> {
            System.out.println("线程1启动,开始判断flag...");
            while (!flag) { // 始终读取工作内存的flag=false,无限循环
            }
            System.out.println("线程1感知到flag修改,退出循环");
        }).start();

        Thread.sleep(1000);
        // 主线程:修改flag,无强制store+write,仅缓存在自身工作内存
        flag = true;
        System.out.println("主线程已将flag修改为true");
    }
}

问题根源:主线程执行assign将flag修改为true后,未立即执行store+write,新值仅存在于主线程的工作内存,主内存的flag仍为false;线程1执行use前,未执行read+load从主内存刷新,始终使用工作内存的flag=false副本,导致无限循环。

(2)加可见性保障的解决场景(volatile修饰,强制操作约束)

volatile的核心作用是为8种操作添加强制执行规则,强制assign后立即store+write,use前立即read+load,无需加锁即可实现轻量的可见性保障:

public class VisibilityTest {
    public static volatile boolean flag = false; // volatile强制操作约束

    public static void main(String[] args) throws InterruptedException {
        // 同上述代码
        new Thread(() -> {
            System.out.println("线程1启动,开始判断flag...");
            while (!flag) { // use前强制read+load,实时刷新最新值
            }
            System.out.println("线程1感知到flag修改,退出循环");
        }).start();

        Thread.sleep(1000);
        flag = true; // assign后强制store+write,立即写回主内存
        System.out.println("主线程已将flag修改为true");
    }
}

保障逻辑

  1. 主线程执行assign将flag修改为true后,因volatile的强制约束,立即执行store+write,将新值同步到主内存,更新主内存的flag为true;
  2. 线程1每次执行use判断flag前,因volatile的强制约束,先执行read+load从主内存重新加载flag值,能及时感知到主内存的true值,从而退出循环;
  3. 整个过程通过8种操作的强制执行顺序,实现了共享变量的可见性。

四、8种原子操作如何保障「有序性」(附案例)

1. 有序性的底层保障原理

有序性的核心是「程序执行顺序符合预期」,JMM对有序性的保障分为单线程多线程两层,均通过8种操作的执行时序约束内存屏障效应实现,核心是「单线程内按序执行、多线程间通过锁/volatile禁止乱序」:

  • 单线程有序:遵循as-if-serial原则,8种操作必须按「read→load→use」「assign→store→write」的固定链路执行,禁止跨链路的指令重排(如不能先use再load,不能先write再assign);
  • 多线程有序:通过lock+unlockvolatile为8种操作添加内存屏障,禁止屏障前后的操作指令重排,同时通过Happens-Before原则,定义操作之间的「先行发生」关系,确保多线程下的执行顺序可预测。

2. 关键操作支撑与核心约束

  • 核心支撑操作:lockunlockreadloadassignstorewrite(lock/unlock触发内存屏障,其余操作通过时序约束避免单线程乱序);
  • 三大核心约束:
    1. 单线程时序约束:8种操作的执行必须遵循「读取链路」和「写回链路」的固定顺序,禁止跨链路重排(如use操作不能出现在load操作之前,store操作不能出现在assign操作之前);
    2. 锁的内存屏障约束:执行lock加锁时,会在锁操作前后插入内存屏障,禁止屏障前后的指令重排;执行unlock解锁时,同样插入内存屏障,确保解锁前的所有操作都已执行完成;
    3. volatile的内存屏障约束:变量被volatile修饰时,read+load前插入读屏障,禁止后续操作重排到读操作之前;store+write后插入写屏障,禁止之前的操作重排到写操作之后。

3. 代码案例:解决DCL单例的指令重排问题

双重检查锁(DCL)实现单例时,若单例对象无volatile修饰,instance = new Singleton()的底层操作会因指令重排出现「半初始化对象」问题,这是典型的有序性缺失;通过volatile修饰对象,可为8种操作添加内存屏障,禁止重排,保障有序性。

(1)无有序性保障的问题场景(DCL单例,无volatile)

public class Singleton {
    private static Singleton instance; // 无volatile,存在指令重排风险

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) { // lock加锁
                if (instance == null) { // 第二次检查
                    // 底层3步操作:1.分配内存 2.初始化对象 3.赋值instance
                    instance = new Singleton(); 
                }
            } // unlock解锁
        }
        return instance; // 可能返回半初始化对象
    }
}

问题根源instance = new Singleton()的底层3步操作,对应JMM的8种操作是「assign(分配内存)→ assign(初始化对象)→ store+write(赋值instance)」,编译器/CPU为优化性能,会将步骤2和步骤3重排(变为1→3→2),导致:

  1. 线程A执行到3时,已执行store+write将instance赋值为非null(但对象未初始化,半初始化状态);
  2. 线程B执行第一次检查时,发现instance≠null,直接返回该半初始化对象,导致空指针、属性值异常等问题;
  3. 核心是无内存屏障约束,8种操作的写回链路(assign→store→write)内部出现了重排

(2)加有序性保障的解决场景(DCL单例,加volatile)

public class Singleton {
    private static volatile Singleton instance; // 加volatile,禁止指令重排

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 禁止步骤2和3重排
                }
            }
        }
        return instance;
    }
}

保障逻辑

  1. volatile为assign→store→write写回链路添加了写屏障禁止将assign(初始化对象)重排到store+write(赋值instance)之后,确保instance = new Singleton()的3步操作按「1.分配内存→2.初始化对象→3.赋值instance」的顺序执行;
  2. 线程A执行到3时,对象已完成初始化,store+write写回的instance是完整的初始化对象
  3. 线程B执行第一次检查时,若instance≠null,必然是已完成初始化的对象,避免了半初始化问题;
  4. 整个过程通过为8种操作添加内存屏障约束,禁止了多线程下的指令重排,保障了执行的有序性。

五、8种原子操作与三大特性保障的核心总结

核心特性 底层保障原理 关键支撑操作 典型保障方式 核心案例
原子性 1. 8种操作自身原生原子;
2. lock+unlock包裹操作序列,独占执行不被打断
lock、unlock、read、load、use、assign、store、write 1. synchronized(底层lock+unlock);
2. Atomic原子类(底层CAS+volatile)
解决i++的丢失更新问题
可见性 1. 修改必写回(assign后立即store+write);
2. 读取必刷新(use前立即read+load);
3. 解锁必同步(unlock前必须store+write)
assign、store、write、read、load、unlock 1. volatile(轻量,强制操作执行顺序);
2. synchronized/LOCK(解锁前强制写回)
解决共享布尔变量的无限循环问题
有序性 1. 单线程内按操作链路有序执行(as-if-serial);
2. lock/unlock/volatile添加内存屏障,禁止多线程重排
lock、unlock、read、load、assign、store、write 1. volatile(添加读写屏障,禁止重排);
2. synchronized/LOCK(锁的内存屏障效应)
解决DCL单例的半初始化对象问题

核心结论:JMM的三大特性并非由某个单独的操作保障,而是通过8种原子操作的原生特性、固定执行时序、严格约束规则,并结合volatilesynchronized等高层语法的封装强化,形成的完整保障体系;8种原子操作是JMM的底层基础,所有并发同步的实现,最终都能映射到这8种操作的组合与执行。

posted @ 2026-01-29 17:18  先弓  阅读(0)  评论(0)    收藏  举报