Appearance
volatile-vs-atomic-vs-lock
1. 实验目标与工程要点
本实验旨在深挖并发三大特性(可见性、有序性、原子性),对比 volatile、AtomicInteger (CAS 原语) 与 synchronized / ReentrantLock (互斥锁) 在不同并发模式下的行为表现,帮助开发者在工程中做出正确的选型抉择。
核心要点总结
volatile仅保证“可见性 + 禁止指令重排序”,不提供“互斥性”,且不保证复合操作的原子性(如count++包含读-改-写 3 步)。AtomicInteger(CAS) 提供无锁原子性,适合高并发下的单变量自增/修改,基于硬件级 CAS(Compare-And-Swap)指令实现,无线程上下文切换开销。synchronized/Lock保证“互斥性 + 原子性 + 可见性”,适合复杂的多字段联动更新或临界区逻辑控制。
2. 深入原理与选型原因分析
2.1 生活浅显比喻:大白板与私人记事本
假设你和同事小王在一个办公室,房间中间有一块大白板(主内存 Main Memory),而你们各自桌上有一本私人记事本(CPU L1/L2 线程高速缓存 Cache)。
1. 为什么会有“不可见”?
默认情况下,为了追求性能:
- 小王想修改数字,他先看白板上的数字是
0,抄到自己的记事本上,在记事本上改为1。 - 如果没有
volatile:小王改完记事本后,觉得“不着急刷回大白板”,过很久才写回。在你眼里,白板上的数字一直还是0。—— 这就是“内存不可见”。
2. volatile 的底层原理是什么?(内存屏障与总线锁/MESI协议)
当变量加上 volatile 关键字后,硬件层面会插入 内存屏障 (Memory Barrier),操作系统和 CPU 会遵守以下铁律:
- 写 volatile 变量时:必须立即把最新的值刷回“大白板”(主内存),同时发信号通知 CPU 把其他所有人的“记事本缓存”强制设为失效。
- 读 volatile 变量时:必须强制抛弃自己记事本里的旧值,重新去“大白板”读取最新的值。
💡 原理总结:
volatile就是强制**“写完立刻弹射回主存” + “读前强制清空本地缓存重新拉取主存”**。
3. 为什么 volatile 保证了最新,count++ 依然会错?(复合操作)
count++ 不是一瞬间完成的动作,它在 CPU 底层包含三步拆解动作:
- 读 (Load):去大白板抄下当前的数字(假设是
10); - 改 (Calculate):在自己的大脑里算出
10 + 1 = 11; - 写 (Store):把
11写回大白板。
竞态过程演示(更新丢失):
- 大白板上
count = 10。 - 你和小王同时想给
count++。 - 步骤 1 (读):你们俩同一时刻去白板看,由于
volatile保证最新,你们俩都看到了10。 - 步骤 2 (改):你们俩都在大脑里算出了
10 + 1 = 11。 - 步骤 3 (写):
- 小王先把
11写回大白板(此时白板变成11); - 紧接着你也把你算好的
11写回大白板(白板依然是11!)。
- 小王先把
- 结果:明明你们两个人各加了一次,期望是
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 多个字段联动一致性修改、跨线程共享复杂缓存控制。
典型代码示例:
kotlinprivate 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. 常见误配与事故注意事项
- ⚠️ 误把
volatile当作线程安全开关- 错误写法:
if (volatileFlag) { volatileCount++; } - 后果:
if条件检查与后续自增/修改不是原子逻辑,高并发下依然会出现竞态条件 (Race Condition)。
- 错误写法:
- ⚠️ 在 Android UI 线程上锁等待
- 后果:主线程尝试获取已被后台线程长任务占用的
synchronized锁,导致 UI 卡顿甚至 ANR (Application Not Responding)。
- 后果:主线程尝试获取已被后台线程长任务占用的
- ⚠️ 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. 对应知识库文档
- 理论主文档:Java 并发模型总览
- CAS 前置概念:CAS 与无锁原子基础