并发同步机制全览
graph LR A[并发同步机制] --> B[锁(悲观锁)] A --> C[volatile] A --> D[CAS(乐观锁)] B --> B1[内核态锁:上下文切换慢,缓存失效] B --> B2[用户态锁:无竞争时有效] B --> B3[synchronized:独占锁,低并发可用] C --> C1[保证可见性] C --> C2[不保证原子性] C --> C3[适合状态标志、配置刷新] D --> D1[乐观锁实现:失败重试,不挂起] D --> D2[CAS原子操作,CPU指令完成] D --> D3[可能出现ABA问题] D3 --> D31[解决:版本号/AtomicStampedReference] D --> D4[应用场景:高并发计数器、累加器] A --> E[高并发优化思路] E --> E1[优化锁实现:ConcurrentHashMap桶粒度锁] E --> E2[Lock-free结构:CAS、Disruptor、无锁队列] E --> E3[单线程循环+异步I/O:Nginx/Node.js模式] E --> E4[内存处理:多级缓存、Redis、读写分离] E --> E5[定时同步缓存到DB] style B fill:#f9f,stroke:#333,stroke-width:1px style C fill:#9f9,stroke:#333,stroke-width:1px style D fill:#ff9,stroke:#333,stroke-width:1px style E fill:#9ff,stroke:#333,stroke-width:1px
锁 vs volatile vs CAS 对比
方案 | 可见性 | 原子性 | 性能 | 适用场景 | 备注 |
---|---|---|---|---|---|
volatile 单独使用 | ✅ | ❌ | ⭐⭐⭐⭐ | 状态标志位、配置刷新、单次写入 | 不能用于依赖旧值的操作(如 count++ ) |
AtomicInteger (CAS) | ✅ | ✅(乐观锁+重试) | ⭐⭐⭐⭐ | 高并发计数器、累加器 | 可能出现自旋重试,适合短时间冲突 |
volatile + synchronized | ✅ | ✅(互斥锁) | ⭐⭐ | 低并发计数器、逻辑简单的临界区 | 存在阻塞,线程切换开销大 |
锁的代价
在并发编程中,锁是最直接的同步手段,但代价也最高
- 内核态锁:涉及上下文切换,CPU 缓存失效,性能损失大;操作系统对多线程锁的判断也相对滞后,响应慢
- 用户态锁:避免上下文切换,但仅在没有真正竞争时有效
Java 1.5 通过 synchronized
实现独占锁(悲观锁),保证线程对共享变量的独占访问。然而,悲观锁存在以下问题:
- 高竞争下,加锁和释放锁会导致上下文切换和调度延时,性能下降
- 一个线程持有锁时,其他线程必须挂起
- 高优先级线程等待低优先级线程释放锁可能导致优先级倒置
volatile 变量
相比锁,volatile
更轻量,因为它不会触发线程调度或上下文切换。但它无法保证原子性,因此当变量更新依赖旧值时不能使用
1. 作用
- 保证可见性:一个线程修改
volatile
变量后,其他线程立即可见 - 禁止编译器/CPU 对
volatile
变量的读写进行危险优化(寄存器缓存、重排序等)
⚠️ 注意:volatile
不保证复合操作的原子性
2. 原子性问题示例
volatile int count = 0;
public void increment() {
count = count + 1; // 或 count++
}
虽然看似只修改一次,实际上拆分为三步:
- 读取旧值
- 计算新值
- 写回
多线程环境下可能出现竞态条件(race condition):
- 线程 A、B 同时读取
count = 10
- A 写回 11,B 写回 11
- 最终
count
只增加 1,而不是 2
✅ 总结一句话:
volatile
解决的是可见性,但复合操作需要原子性。一旦变量的写入依赖旧值,volatile
就不够了,必须借助锁或原子类
%%{init: {"sequence": {"rightAngles":true,"width":350}}}%% sequenceDiagram participant MainMemory as 主内存(count=10) participant ThreadA as 线程A participant ThreadB as 线程B ThreadA->>MainMemory: 读取 count (10) ThreadB->>MainMemory: 读取 count (10) ThreadA->>ThreadA: 计算 10+1=11 ThreadB->>ThreadB: 计算 10+1=11 ThreadA->>MainMemory: 写回 11 ThreadB->>MainMemory: 写回 11
即使 volatile
保证了线程 A 写入 11 后能立即对 B 可见,但因为两个线程都基于旧值 10 来计算,导致结果丢失一次更新
3. 解决方案
- 锁机制:
synchronized
或Lock
- CAS 原子类:
AtomicInteger
、AtomicLong
CAS 示例(AtomicInteger.incrementAndGet)
sequenceDiagram participant MainMemory as 主内存(count=10) participant ThreadA as 线程A participant ThreadB as 线程B Note over MainMemory: count 初始值 = 10 ThreadA->>MainMemory: 读取 count (10) ThreadB->>MainMemory: 读取 count (10) ThreadA->>MainMemory: CAS(预期=10, 新值=11) → 成功 Note over MainMemory: count = 11 ThreadB->>MainMemory: CAS(预期=10, 新值=11) → 失败 (因当前值=11) ThreadB->>MainMemory: 重新读取 count=11 ThreadB->>MainMemory: CAS(预期=11, 新值=12) → 成功 Note over MainMemory: count = 12 (正确)
volatile + synchronized
sequenceDiagram participant MainMemory as 主内存(count=10) participant ThreadA as 线程A participant ThreadB as 线程B Note over MainMemory: count 初始值 = 10 ThreadA->>ThreadA: synchronized 加锁 ThreadA->>MainMemory: 读取 count (10) ThreadA->>ThreadA: 计算 10+1 = 11 ThreadA->>MainMemory: 写回 11 ThreadA->>ThreadA: 释放锁 Note over MainMemory: count = 11 ThreadB->>ThreadB: synchronized 加锁 (等待A释放) ThreadB->>MainMemory: 读取 count (11) ThreadB->>ThreadB: 计算 11+1 = 12 ThreadB->>MainMemory: 写回 12 ThreadB->>ThreadB: 释放锁 Note over MainMemory: count = 12 (正确)
变量更新方式对比
方案 | 可见性 | 原子性 | 性能 | 适用场景 | 备注 |
---|---|---|---|---|---|
volatile 单独使用 | ✅ 保证 | ❌ 不保证 | ⭐⭐⭐⭐ 高 | 状态标志位、配置刷新、单次写入 | 不能用于依赖旧值的操作(如 count++ ) |
AtomicInteger (CAS) | ✅ 保证 | ✅ 保证(乐观锁+重试) | ⭐⭐⭐⭐ 较高 | 高并发计数器、累加器 | 可能出现自旋重试,适合短时间冲突 |
volatile + synchronized | ✅ 保证 | ✅ 保证(互斥锁) | ⭐⭐ 一般 | 低并发下的计数器、逻辑简单的临界区 | 存在阻塞,线程切换开销大 |
只需要可见性 → 用 volatile
需要原子性 → 首选 AtomicInteger
逻辑复杂 or 低并发 → synchronized
CAS(Compare-And-Swap)
CAS 是一种乐观锁实现方式:
- 基于共享数据不会被修改的假设
- 采用 commit-retry 模式,线程失败不会挂起,可再次尝试
CAS 开销
CAS 基于 CPU 指令,原子操作在 CPU 内部完成,非常快,避免了 OS 内核锁裁定。但存在 cache miss 和 ABA 问题
CPU 缓存线竞争
当 CAS 操作变量所在 cache line 不在本地 CPU 时,需要跨 CPU 和系统互联模块请求缓存线,增加延迟
ABA 问题
问题:CAS 只检查内存当前值是否等于预期,忽略了中间可能发生的修,如果在此过程中值由 A→B→A,CAS 会误判成功
解决方案:使用版本号(version)或 AtomicStampedReference
,每次修改都更新版本号,避免 ABA
示例:AtomicInteger vs AtomicStampedReference
AtomicInteger atomicInt = new AtomicInteger(100);
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0);
-
AtomicInteger
CAS 会成功(ABA 问题存在) -
AtomicStampedReference
CAS 会失败(版本号保护,避免 ABA)
jvm 对CAS的支持
-
JDK 1.5 引入底层 CAS 支持:
int
、long
、对象引用等 -
CPU 不支持 CAS 时,JVM 使用自旋锁
-
AtomicLong.incrementAndGet()
使用sun.misc.Unsafe
提供的 CAS 指令,无锁自增,比synchronized
效率高数倍
高并发优化思路
高并发下性能杀手:
- 线程切换开销
- 锁
- 非必要内存拷贝
优化方向:
- 优化锁实现
- 例如
ConcurrentHashMap
使用桶粒度锁、锁分离,避免整个 map 锁定
- 例如
- Lock-free 无锁结构
- 利用 CAS、Disruptor 等无锁队列
- 或单线程大循环(Nginx、Node.js)+ 异步 I/O
优化目标:
- 减少资源争用
- 任务尽量在内存中处理(多级缓存、读写分离、ConcurrentHashMap、Redis)
- 定时同步缓存到数据库
对于大型分布式系统,需要分而治之,SOA 思路更适合管理复杂的并发场景