JMM 三大特性(原子性 / 可见性 / 有序性)面试精简版

本文为精简版,需要看具体原理的看我这篇文章:https://blog.csdn.net/Chang_Yafei/article/details/128184249?ops_request_misc=%257B%2522request%255Fid%2522%253A%25224a664394e785be835f5fe37ca63221b5%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=4a664394e785be835f5fe37ca63221b5&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-2-128184249-null-null.nonecase&utm_term=JMM&spm=1018.2226.3001.4450

一、核心概念总述

Java 内存模型(JMM)定义了共享变量在多线程间的访问规则 ,原子性、可见性、有序性是保障多线程并发安全的三大核心特性。三者的目标是解决多线程环境下因工作内存与主内存隔离指令重排操作拆分导致的并发问题。

二、原子性

1. 定义

一个或多个操作,要么全部执行且执行过程中不被任何线程打断,要么全部不执行,即操作是 "不可分割" 的最小执行单元。

2. 核心问题(反例)

多线程下的复合操作会被拆分,导致结果异常。

  • 例如:count++ 实际分为 3 步:
    1. 从主内存读取 count 到线程工作内存;
    2. 工作内存中执行 count+1
    3. 将结果写回主内存。
  • 多线程执行时,步骤会被交叉打断,导致最终结果小于预期值。

3. 保障机制

保障手段 原理说明 适用场景
synchronized 关键字 通过monitorenter/monitorexit指令实现互斥锁,同一时刻仅一个线程进入同步块,同步块内所有操作串行执行,视为原子整体。 所有需要原子性的复合操作(如count++、多步业务逻辑)
Lock 接口(如 ReentrantLock) 基于 AQS 实现的显式锁,通过lock()加锁、unlock()解锁保证互斥,效果与synchronized一致,支持更灵活的锁控制。 需超时锁、公平锁等高级功能的场景
CAS(原子类,如 AtomicInteger) 基于 CPU 的Compare-And-Swap硬件指令,无锁实现原子操作:比较内存值与预期值,相等则更新,否则重试。 高频读、低频写的简单原子操作(如计数器)
JMM 原生支持 基本数据类型(long/double除外)的单次读写操作 天然具备原子性(如int a = 1)。 简单变量的单次赋值 / 读取

4. 面试关键点

  • long/double 类型的单次读写不保证原子性 (JVM 允许拆分为两次 32 位操作),需用volatile或原子类保障。
  • volatile 不保证原子性,仅能保证可见性和有序性。

三、可见性

1. 定义

一个线程修改了共享变量的值,其他线程能立刻看到该修改后的最新值 ,解决多线程因工作内存与主内存隔离导致的 "数据不一致" 问题。

2. 核心问题(反例)

JMM 中,线程操作共享变量时,先将变量从主内存 拷贝到线程工作内存,修改后再写回主内存。若未保证可见性:

  • 线程 A 修改了工作内存中的变量,但未及时写回主内存;
  • 线程 B 仍从主内存读取旧值,导致线程 B 无法感知线程 A 的修改。

3. 保障机制

保障手段 原理说明 适用场景
volatile 关键字 1. 写屏障 :线程写volatile变量时,强制将工作内存中的最新值刷新到主内存;2. 读屏障 :线程读volatile变量时,强制失效工作内存中的旧值,必须从主内存重新读取。 读多写少的状态标记(如volatile boolean flag控制线程启停)
synchronized 关键字 1. 加锁时 :失效当前线程工作内存的共享变量,从主内存重新读取;2. 解锁时 :强制将工作内存的修改刷新到主内存;3. 结合happen-before 原则:同一锁的解锁操作 happen-before 于后续加锁操作,确保修改被后续线程感知。 需同时保证原子性 + 可见性的场景
Lock 接口 synchronized原理一致,unlock()时强制刷新主内存,结合happen-before原则保障可见性。 显式锁场景
final 关键字 final修饰的变量,初始化完成后(无逸出),其他线程能看到其最终值,不可被修改。 不可变变量的可见性保障

4. 面试关键点

  • volatile 的可见性不依赖锁,是轻量级方案,但无法解决复合操作的原子性问题。
  • 可见性的本质是强制内存同步,打破 "工作内存与主内存的隔离"。

四、有序性

1. 定义

程序执行的顺序,符合代码的编写顺序 ,解决因编译器优化CPU指令重排导致的 "代码执行顺序混乱" 问题。

2. 核心问题:指令重排

为提升执行效率,编译器和 CPU 会在不改变单线程执行结果的前提下,对指令执行顺序进行重排。但多线程下,重排会导致执行逻辑与预期不符。

  • 典型反例:双重检查锁(DCL)单例模式

    java

    运行

    复制代码
    public class Singleton {
        private static Singleton instance; // 未加volatile
        public static Singleton getInstance() {
            if (instance == null) { // 第一次检查
                synchronized (Singleton.class) {
                    if (instance == null) { // 第二次检查
                        instance = new Singleton(); // 指令重排风险
                    }
                }
            }
            return instance;
        }
    }
    • instance = new Singleton() 实际分为 3 步:
      1. 分配内存空间;
      2. 初始化对象;
      3. instance指向内存地址。
    • 编译器可能重排为 1→3→2:线程 A 执行到 3 时,instance 已非null,但对象未初始化;此时线程 B 进入第一次检查,会获取到 "半初始化" 的对象,导致程序异常。

3. 保障机制

保障手段 原理说明 适用场景
volatile 关键字 通过内存屏障 禁止指令重排:1. 写volatile变量时,禁止其之前的指令重排到之后;2. 读volatile变量时,禁止其之后的指令重排到之前。 防止指令重排的场景(如 DCL 单例的instance变量)
synchronized 关键字 1. 同步块内代码串行执行 ,不存在多线程指令交叉,自然符合编写顺序;2. 结合happen-before原则:前线程同步块内的操作顺序,会传递给后续获取同一锁的线程。 需同时保证原子性 + 有序性的场景
happen-before 原则 JMM 的核心规则,定义了操作之间的 "先后顺序",满足该原则则保证有序性。关键规则:1. 解锁操作 happen-before 后续同一锁的加锁操作;2. volatile变量的写操作 happen-before 后续的读操作;3. 线程内操作按代码顺序 happen-before。 所有有序性保障的底层逻辑

4. 面试关键点

  • 指令重排不影响单线程执行结果,仅在多线程下引发问题。
  • DCL 单例模式中,instance 必须加volatile,否则会因指令重排导致 "半初始化对象" 问题。

五、三大特性与核心关键字能力对比(面试必背)

特性 synchronized volatile AtomicInteger(CAS) Lock 接口
原子性 ✅ 支持复合操作 ❌ 不支持 ✅ 支持简单原子操作 ✅ 支持复合操作
可见性
有序性 ❌ 不保证
核心优势 全能型,无需手动释放锁 轻量级,无锁 无锁,高性能 灵活控制(公平锁 / 超时锁)
核心劣势 重量级,可能引发上下文切换 不支持原子性 高并发下可能自旋消耗 CPU 需手动释放锁,易遗漏

六、面试答题思路总结

  1. 先定义:分别解释原子性(不可分割)、可见性(修改立即可见)、有序性(执行顺序符合代码)。
  2. 讲问题 :结合反例说明不保障三大特性的后果(如count++原子性问题、线程看不到修改的可见性问题、DCL 的有序性问题)。
  3. 说机制 :分点阐述每种特性的保障手段,重点对比synchronizedvolatile的差异。
  4. 划重点 :强调volatile不保证原子性、DCL 必须加volatilesynchronized是全能型锁。

面试追问问题:如果synchronized是全能的,那为什么DCL要加volatile?

一、核心结论

synchronized的 "有序性保障" 有边界 :它只能保证同步块内的代码串行执行 ,但无法禁止同步块内的指令重排 (只要重排不影响单线程执行结果);而 DCL 的问题恰恰出在instance = new Singleton()的指令重排上,这个重排发生在synchronized同步块内,synchronized管不住,必须靠volatile禁止重排。

二、拆解 DCL 的执行逻辑(为什么 synchronized 不够)

先回顾 DCL 的核心代码:

复制代码
public class Singleton {
    // 关键:不加volatile会出问题
    private static Singleton instance; 

    public static Singleton getInstance() {
        if (instance == null) { // ① 第一次检查(无锁)
            synchronized (Singleton.class) { // ② 加锁
                if (instance == null) { // ③ 第二次检查(加锁后)
                    instance = new Singleton(); // ④ 问题核心:指令重排
                }
            } // ⑤ 解锁
        }
        return instance; // ⑥ 返回实例
    }
}

1. instance = new Singleton()的真实指令(3 步)

这行代码看似是 "一步赋值",实际被编译器 / CPU 拆分为 3 条指令:

plaintext

复制代码
1. 分配内存空间(给Singleton对象);
2. 初始化对象(执行构造方法,给属性赋值);
3. 将instance引用指向刚分配的内存地址(此时instance≠null)。

2. 编译器的 "合法重排"(synchronized 管不住)

为了提升效率,编译器在不改变单线程执行结果的前提下,会把指令重排为:

plaintext

复制代码
1. 分配内存空间;
3. 将instance指向内存地址(instance≠null);
2. 初始化对象。

⚠️ 重点:这个重排发生在 synchronized 同步块内 ,但synchronized只保证 "同步块内的代码串行执行",不禁止 "单线程内的指令重排"------ 因为重排后单线程执行结果没变(最终 instance 都指向初始化后的对象),所以 JVM 允许这种优化。

3. 重排导致的线程安全问题(synchronized 无法解决)

假设有两个线程 A、B 同时调用getInstance()

线程 A(进入同步块) 线程 B(未进入同步块)
执行指令 1:分配内存 -
执行指令 3:instance≠null(但对象未初始化) -
准备执行指令 2:初始化对象(还没执行) 执行①第一次检查:发现 instance≠null → 直接返回 instance
执行指令 2:初始化对象 线程 B 拿到 "半初始化" 的对象,调用其方法时 NPE / 数据异常

此时synchronized的作用完全失效:

  • synchronized保证了 "只有线程 A 能进入同步块执行初始化",但管不住 "线程 B 在同步块外的第一次检查";
  • 线程 B 看到的instance≠null是重排后的 "虚假非空",此时对象还没初始化,导致程序崩溃。

4. volatile 的 "兜底作用"

instancevolatile后:

复制代码
private static volatile Singleton instance;

volatile通过内存屏障 禁止了上述指令重排,强制指令按1→2→3的顺序执行 ------ 只有当对象完全初始化(指令 2 执行完)后,instance 才会被赋值为非 null。

此时线程 B 的第一次检查:

  • 要么看到instance=null(进入同步块排队);
  • 要么看到instance≠null(此时对象已完全初始化);彻底避免了 "半初始化对象" 问题。

三、synchronized vs volatile:有序性保障的边界(面试必背)

特性 synchronized volatile
有序性保障范围 1. 同步块内代码串行执行(多线程无交叉);2. 解锁操作 happen-before 后续加锁操作(保证顺序可见性)。 禁止被修饰变量的指令重排(无论是否在同步块内)。
对 "单线程指令重排" 的态度 允许(只要单线程结果不变) 禁止(针对被修饰变量的读写指令)
在 DCL 中的作用 保证 "只有一个线程执行初始化"(原子性) 禁止初始化指令重排(有序性)

总结

  1. synchronized在 DCL 中只解决了 "原子性" 问题(保证只有一个线程初始化对象),但解决不了 "有序性" 的核心问题(指令重排导致的半初始化对象);
  2. volatile的核心价值是禁止指令重排 ,补上了synchronized在 "单线程指令重排" 上的漏洞;
  3. DCL 必须同时用synchronized(保证原子性)+volatile(保证有序性),缺一不可。
相关推荐
该怎么办呢2 小时前
基于cesium的三维不动产登记系统的设计与实现(毕业设计)
java·毕业设计
J不A秃V头A3 小时前
多任务执行时,共享请求对象被并发修改
java
heartbeat..3 小时前
零基础学 SQL:DQL/DML/DDL/DCL 核心知识点汇总(附带连接云服务器数据库教程)
java·服务器·数据库·sql
专注于大数据技术栈3 小时前
java学习--LinkedHashSet
java·开发语言·学习
阿湯哥4 小时前
Spring AI Alibaba 实现 Workflow 全指南
java·人工智能·spring
Ey4434 小时前
2-03SQL注入漏洞------------2
面试
一颗青果4 小时前
进程组 | 会话 |终端 | 前台后台 | 守护进程
linux·运维·jvm
旺仔小拳头..4 小时前
Java ---变量、常量、类型转换、默认值、重载、标识符、输入输出、访问修饰符、泛型、迭代器
java·开发语言·python
12344524 小时前
【面试复盘】有了equals为什么还要hashcode
java·后端