Skip to content

volatile-vs-atomic-vs-lock

1. 实验目标与工程要点

本实验旨在深挖并发三大特性(可见性、有序性、原子性),对比 volatileAtomicInteger (CAS 原语) 与 synchronized / ReentrantLock (互斥锁) 在不同并发模式下的行为表现,帮助开发者在工程中做出正确的选型抉择。

核心要点总结

  1. volatile 仅保证“可见性 + 禁止指令重排序”,不提供“互斥性”,且不保证复合操作的原子性(如 count++ 包含读-改-写 3 步)。
  2. AtomicInteger (CAS) 提供无锁原子性,适合高并发下的单变量自增/修改,基于硬件级 CAS(Compare-And-Swap)指令实现,无线程上下文切换开销。
  3. synchronized / Lock 保证“互斥性 + 原子性 + 可见性”,适合复杂的多字段联动更新或临界区逻辑控制。

2. 深入原理与选型原因分析

2.1 生活浅显比喻:大白板与私人记事本

假设你和同事小王在一个办公室,房间中间有一块大白板(主内存 Main Memory),而你们各自桌上有一本私人记事本(CPU L1/L2 线程高速缓存 Cache)

1. 为什么会有“不可见”?

默认情况下,为了追求性能:

  • 小王想修改数字,他先看白板上的数字是 0,抄到自己的记事本上,在记事本上改为 1
  • 如果没有 volatile:小王改完记事本后,觉得“不着急刷回大白板”,过很久才写回。在你眼里,白板上的数字一直还是 0。—— 这就是“内存不可见”

2. volatile 的底层原理是什么?(内存屏障与总线锁/MESI协议)

当变量加上 volatile 关键字后,硬件层面会插入 内存屏障 (Memory Barrier),操作系统和 CPU 会遵守以下铁律:

  1. 写 volatile 变量时:必须立即把最新的值刷回“大白板”(主内存),同时发信号通知 CPU 把其他所有人的“记事本缓存”强制设为失效
  2. 读 volatile 变量时:必须强制抛弃自己记事本里的旧值,重新去“大白板”读取最新的值。

💡 原理总结volatile 就是强制**“写完立刻弹射回主存” + “读前强制清空本地缓存重新拉取主存”**。

3. 为什么 volatile 保证了最新,count++ 依然会错?(复合操作)

count++ 不是一瞬间完成的动作,它在 CPU 底层包含三步拆解动作

  1. 读 (Load):去大白板抄下当前的数字(假设是 10);
  2. 改 (Calculate):在自己的大脑里算出 10 + 1 = 11
  3. 写 (Store):把 11 写回大白板。

竞态过程演示(更新丢失):

  1. 大白板上 count = 10
  2. 和小王同时想给 count++
  3. 步骤 1 (读):你们俩同一时刻去白板看,由于 volatile 保证最新,你们俩都看到了 10
  4. 步骤 2 (改):你们俩都在大脑里算出了 10 + 1 = 11
  5. 步骤 3 (写)
    • 小王先把 11 写回大白板(此时白板变成 11);
    • 紧接着你也把你算好的 11 写回大白板(白板依然是 11!)。
  6. 结果:明明你们两个人各加了一次,期望是 12,但白板上最终却是 11!—— 这就是“更新丢失 (Lost Update)”

2.2 三种并发机制的解决思路比喻与底层对比

机制解决思路(通俗比喻)底层原理保证特性吞吐性能运行开销
volatile透明公开:规定“写了立刻挂白板,读前必须看白板”。不阻止别人插手内存屏障 (Memory Barrier) + MESI 缓存一致性协议。可见性 + 阻止重排序极高(无锁非阻塞)📉 极低(仅触发 CPU 缓存刷盘)
AtomicInteger (CAS 原语)乐观重试:去写白板时问一句“白板还是我看到的 10 吗?是就改为 11;被改了就重读并重算”。硬件 CPU 指令 LOCK CMPXCHG(Compare-And-Swap 原子比较替换),无锁自旋。可见性 + 单变量原子性🚀 较高(无线程阻塞)📊 中等(高竞争下自旋消耗 CPU)
synchronized / Lock悲观排他锁:小王给 count++ 前先把走廊锁上,做完“读改写”三步开锁后别人才能进。操作系统级别 JVM 监视器锁 (Monitor) / Thread 挂起与唤醒。可见性 + 原子性 + 互斥性🐢 较低(失败线程需排队阻塞)📈 较高(频繁触发线程上下文切换开销)

3. Android / 移动端实战应用场景与知识点

在 Android 应用开发中,线程模型分为 UI 线程 (Main Thread)工作线程池 / Background Worker,针对这三种原语的选型直接影响性能与稳定性:

3.1 volatile

  • 适用 Android 场景:后台轮询/采集任务停止标记位、全局不可变配置快照更新、DCL 延迟初始化单例。

  • 典型代码示例

    kotlin
    // 1. 任务停止开关(单写多读)
    @Volatile
    private var isTaskStopped = false
    
    // 2. 全局不可变配置快照引用整体替换
    @Volatile
    var currentConfig: AppConfig = AppConfig(...)
  • 选型原因与注意点:极轻量,无锁无阻塞开销。必须满足“单写多读”或“变量间互相独立赋值”。不可用于点赞数累加或多字段 UI State 联动更新。

3.2 AtomicInteger / AtomicReference

  • 适用 Android 场景:点赞数累加、下载进度统计、并发任务完成计数器、线程安全状态机。

  • 典型代码示例

    kotlin
    // 1. 并发点赞/计数器
    val downloadCount = AtomicInteger(0)
    downloadCount.incrementAndGet()
    
    // 2. 线程安全状态切换
    val state = AtomicReference(State.IDLE)
    state.compareAndSet(State.IDLE, State.RUNNING)
  • 选型原因与注意点:基于硬件级 CAS 无锁原语,高并发下吞吐量高于加锁。但极端高竞争下 CAS 循环自旋可能消耗 CPU 资源;且只保障单变量原子性,无法保障多个 Atomic 变量之间的协同原子性。

3.3 synchronized / ReentrantLock

  • 适用 Android 场景:数据库/本地文件写入互斥、UI State 多个字段联动一致性修改、跨线程共享复杂缓存控制。

  • 典型代码示例

    kotlin
    private val lock = Any()
    
    fun updateUiState(newData: List<String>) {
        synchronized(lock) {
            // 保证 isLoading 与 data 两个字段变更的原子性与一致性
            uiState = uiState.copy(isLoading = false, data = newData)
        }
    }
  • 选型原因与注意点:表达能力最强,保障临界区多字段复合逻辑。但严禁在 UI 主线程持有锁等待耗时后台操作,否则可能引发 ANR。

💡 Android 现代开发演进与文档跳转: 在 Kotlin / Coroutines 场景下,传统的 Java 原生锁与 volatile 存在替代升级方案:

  • 状态管理推荐优先采用 StateFlow / SharedFlow 配合不可变数据快照 (copy),实现线程安全的状态流分发;
  • 临界区互斥推荐使用协程的 Mutex(非阻塞挂起锁,避免阻塞 OS 线程引发卡顿/ANR)替换 Java 原生 synchronized / ReentrantLock

详见 Kotlin 协程并发与挂起机制主文档:Kotlin 协程挂起恢复与并发 · 可运行对照:coroutine-mutex-stateflow


4. 常见误配与事故注意事项

  1. ⚠️ 误把 volatile 当作线程安全开关
    • 错误写法if (volatileFlag) { volatileCount++; }
    • 后果if 条件检查与后续自增/修改不是原子逻辑,高并发下依然会出现竞态条件 (Race Condition)。
  2. ⚠️ 在 Android UI 线程上锁等待
    • 后果:主线程尝试获取已被后台线程长任务占用的 synchronized 锁,导致 UI 卡顿甚至 ANR (Application Not Responding)。
  3. ⚠️ DCL 单例遗漏 volatile
    • 后果:未加 volatile 的单例引用可能因指令重排序,导致其他线程拿到一个“已分配内存但未完成构造函数初始化”的半成品对象,引发 NullPointerException 或逻辑异常。

5. 实验源码与运行验证

关键源码

运行方式

在当前目录下执行以下命令编译并运行:

bash
cd labs/java/runtime-concurrency/volatile-vs-atomic-vs-lock
javac -d out src/VolatileVsAtomicVsLock.java
java -cp out VolatileVsAtomicVsLock

预期控制台运行输出

text
====== 场景 1:volatile 适用 - 状态标记位/停止开关 ======
【状态标记测试】工作线程接收到停止信号,退出循环!本地计数累加次数:20243084
【状态标记测试】主线程成功让工作线程停止运行。

====== 场景 2:volatile 不适用 - 高并发自增计数 ======
期待结果 (Expected Count)   : 10000
volatileCount 实际结果      : 9954 (由于非原子操作导致更新丢失)
atomicCount 实际结果        : 10000 (CAS 保证原子性)
syncCount 实际结果          : 10000 (锁保证原子性与互斥)

6. 对应知识库文档