Java 并发编程:从 Lock 到 AQS
并发编程里,“锁”是跨线程协调和内存可见性的核心抽象。本文从 Java 对象头与锁位开始,系统梳理锁信息的存放位置、synchronized 与 Lock/AQS 的实现原理、CAS 的内存语义与常见陷阱,并从 x86/ARM 的汇编视角出发,解释 HotSpot 在不同平台上的底层逻辑。最后给出工程实践的选型建议与调优要点。
1. Java 对象、对象头与锁位
HotSpot 中对象的内存布局通常包含三段:
- 对象头(Header):包含
Mark Word和Klass Pointer;数组对象还包含数组长度。 - 实例数据(Instance Data):各字段的实际存储。
- 对齐填充(Padding):保证对象按 8 字节对齐。
1.1 Mark Word 与锁信息
Mark Word 是一个会随对象状态复用的位段(32/64 位 JVM 分别是 32/64 位;在 64 位下如果启用指针压缩,Klass Pointer 为 32 位):
- 无锁:存放对象哈希(identity hash code)、GC Age 等。
- 偏向锁:存放偏向线程 ID、Epoch、Age 等。
- 轻量级锁:存放指向线程栈上“锁记录(Lock Record)”的指针(Displaced Header)。
- 重量级锁:存放指向 Monitor(对象监视器)的指针。
锁标志位(lock bits)与偏向标志位(biased bit)共同决定 Mark Word 当前的语义。需要注意:
- 一旦对象计算过 identity hash code(如调用过
System.identityHashCode,或对象参与了基于哈希的容器),Mark Word 需要存放 hash,偏向锁将无法使用(会导致偏向撤销或直接进入轻量级/重量级路径)。 - 锁状态是“可升级、不可降级”的:无锁 → 偏向 → 轻量级 → 重量级。
1.2 JDK 版本对偏向锁的影响
- JDK 6~8:偏向锁默认启用(可通过
-XX:-UseBiasedLocking关闭)。 - JDK 15:根据 JEP 374,偏向锁被默认禁用并标记为废弃。
- JDK 18 起:HotSpot 中移除了偏向锁实现(仅保留语义历史说明)。
工程上撰写面向“现代 JDK(11/17/21 LTS 及以上)”的代码时,可不再依赖偏向锁的收益模型,更多关注轻量级/重量级路径与锁粗细粒度的取舍。
2. synchronized 的状态流转与 Monitor
synchronized 基于对象监视器(Monitor)实现。HotSpot 会根据竞争情况在不同状态间切换:
1) 无锁:进入同步块首次尝试;
2) 轻量级锁:线程在自己的栈帧创建“锁记录”,用 CAS 将对象头替换为指向锁记录的指针;
3) 自旋:竞争不重时短暂自旋等待可避免阻塞开销;
4) 重量级锁:自旋失败或竞争激烈时膨胀为 Monitor,失败线程进入阻塞,等待 unpark/notify 唤醒。
Monitor 内部有两个队列概念:
- 入口队列(EntryList):在
synchronized入口处等待获取锁的线程。 - WaitSet:调用
Object.wait()释放锁并等待条件的线程集合,被notify/notifyAll转移回 EntryList。
现代 HotSpot 中,阻塞/唤醒通常经由 Unsafe.park/unpark 实现,底层在 Linux 使用 futex,在 macOS 使用 pthread 条件变量等原语。
2.1 轻量级锁细节(Lock Record)
轻量级锁是“乐观地假设不存在并发”。流程: 1) 将对象头的 Mark Word 复制到线程栈的锁记录。 2) 使用 CAS 将对象头替换为指向锁记录的指针。 3) 成功即获得锁;失败说明存在竞争,进入自旋或膨胀。 4) 解锁时尝试用 CAS 将对象头还原为锁记录中保存的 Displaced Header;失败则说明发生竞争,转重量级解锁路径。
轻量级锁的优势是在“短临界区、低冲突”场景下显著减少阻塞/唤醒的系统开销。
3. 从 CAS 谈起:原理、内存语义与陷阱
CAS(Compare-And-Swap/Exchange)是硬件提供的原子读-改-写指令族。以三元组 (V, A, B) 描述:当且仅当 V==A 时,将 V 置为 B;否则失败。Java 中的 CAS 主要通过 Unsafe/VarHandle 暴露:
// Java 9+ VarHandle 示例
class Counter {
private volatile int value;
private static final VarHandle VH;
static {
try {
VH = MethodHandles.lookup()
.in(Counter.class)
.findVarHandle(Counter.class, "value", int.class);
} catch (Exception e) { throw new Error(e); }
}
public int increment() {
int prev;
do {
prev = (int) VH.getVolatile(this);
} while (!VH.compareAndSet(this, prev, prev + 1));
return prev + 1;
}
}
3.1 内存语义
不同于“互斥”,CAS 主要提供“原子性 + 指定的有序性”。HotSpot 在不同平台下映射为:
- x86(TSO):天然提供较强顺序性,
LOCK CMPXCHG隐含 acquire-release 语义;必要时配合LFENCE/SFENCE/MFENCE。 - ARMv8:使用 LL/SC 族指令
LDAXR/STLXR(带 acquire/release 语义)与DMB ish栅栏保证有序性。
Java 语言层面,volatile 写具有“release”语义,读具有“acquire”语义;CAS 通常等价于“读-改-写的原子性 + acquire-release”。这保证了临界区内写入对随后持有同一变量可见。
3.2 ABA 问题与对策
CAS 的经典陷阱是 ABA:值从 A→B→A,单次 CAS 无法察觉变化。对策包括:
- 版本戳(如
AtomicStampedReference)、标记指针(AtomicMarkableReference)。 - 结构性约束(避免重用节点)、配合 GC 的安全点检查降低风险。
3.3 多变量一致性
CAS 天然只能覆盖单内存位置。多字段一致性可用:
- 粗粒度互斥(单锁包裹),简单可靠;
- 组合状态编码(如将两字段打包到 64 位 long);
- STM/事务日志(较重,工程中少见)。
4. Lock 与 AQS:CLH 队列、独占/共享与条件队列
AQS(AbstractQueuedSynchronizer)是 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、StampedLock(部分实现)等的基础设施。其核心是:
- 一个
int state表示同步状态(独占/共享语义由子类定义); - 一个基于 CLH 的双向 FIFO 同步队列,失败线程入队并
park; - 成功释放时按队头顺序
unpark,维持有界公平性。
4.1 独占与共享
- 独占(Exclusive):如
ReentrantLock。tryAcquire/tryRelease由子类实现;重入通过把state作为重入计数。 - 共享(Shared):如
Semaphore、CountDownLatch。共享获取可同时唤醒多个等待者。
4.2 公平 vs 非公平
Lock fair = new ReentrantLock(true);
Lock unf = new ReentrantLock(false);
- 公平:严格遵循队列顺序,等待时间方差小,但吞吐稍低;
- 非公平:允许插队(
tryAcquire先试一次),吞吐更高,极端情况下存在饥饿风险。
4.3 条件队列(Condition)
ConditionObject 为每个条件维护独立等待队列:
await():原子地释放主锁、入条件队列并park;signal():将条件队列的首节点转移回同步队列,等待重新竞争主锁。
4.4 AQS 获取/释放(独占)骨架
1) 快路径:CAS 修改 state 成功直接获得;
2) 失败入队:按 CLH 入同步队列,park 自己;
3) 被前驱释放 unpark 后,竞争重试;
4) 释放:tryRelease 成功则唤醒后继。
5. 平台与汇编视角:x86 与 ARM 的差异
5.1 x86(TSO)
- 原子指令:
LOCK XCHG/CMPXCHG/ADD等,LOCK前缀保证跨核原子性与缓存一致性协议的正确传播。 - 自旋优化:热点代码会插入
PAUSE(rep; nop)降低功耗与总线竞争。 - 内存模型:TSO 比 Java 的 JMM 更强,编译器仍需在 volatile/CAS 周边插入恰当屏障以维持 JMM 语义。
5.2 ARMv8(弱内存序)
- LL/SC:
LDXR/STXR,带 acquire/release 版本LDAXR/STLXR;失败返回标志,需循环重试。 - 内存屏障:
DMB ish/DSB/ISB控制可见性与排序。 - Java 映射:VarHandle 的 acquire/release 泛化到上述指令与屏障组合。
6. 不同锁形态的应用场景与选型
synchronized:- 优点:语法简单,异常安全,JIT 内联友好;JDK 近年大量优化,开销显著下降。
- 适用:绝大多数互斥场景,特别是短临界区、低到中等竞争强度。
ReentrantLock:- 优点:可中断、可定时、可选公平,配
Condition多条件队列,诊断性更强。 - 适用:
- 需要可中断获取(避免死等 IO);
- 需要定时超时放弃;
- 需要多个条件队列;
- 需要公平策略限制尾延时。
- 优点:可中断、可定时、可选公平,配
ReentrantReadWriteLock:- 读多写少、读路径可并行;注意“读锁降级、写锁升级”的语义与死锁风险。
StampedLock:- 乐观读避免锁竞态下的写者阻塞;需要二次校验,且不支持重入/条件队列,使用门槛更高。
- CAS/无锁结构:
- 原子类(
Atomic*)、LongAdder/LongAccumulator(热点分散)在高并发计数上优于单点 CAS; - 适用读多写少或对延迟极敏感的路径;需警惕 ABA 与活锁,必要时退避回退或限次自旋转阻塞。
- 原子类(
7. 性能与可见性:几个常见问题
- 自旋与阻塞的取舍:
- 临界区短、竞争偶发:倾向自旋(轻量级锁);
- 临界区长、竞争激烈:尽快阻塞,减少 CPU 浪费与抖动(重量级路径/AQS 直接
park)。
- 假共享(False Sharing):
- 计数热点应使用
LongAdder或通过@jdk.internal.vm.annotation.Contended(或手工填充)隔离写热点,避免不同核心在同一 cache line 争用。
- 计数热点应使用
- 粗细粒度:
- 业务上首先拆分为“无共享”的并行单元;无法拆分时,优先读写分离、分段锁/哈希分片;确需全局一致时再集中化。
- 可见性与发布:
- 共享数据通过
volatile/CAS/锁保护发布;避免未初始化对象逸出; - 使用
final字段保证构造后安全发布的不可变性。
- 共享数据通过
8. 代码片段与基准示例
8.1 synchronized 与 ReentrantLock 对比
// synchronized 版
class CounterS {
private int x;
public synchronized void inc() { x++; }
public synchronized int get() { return x; }
}
// ReentrantLock 版(可中断/可定时)
class CounterL {
private final ReentrantLock lock = new ReentrantLock();
private int x;
public void inc() {
lock.lock();
try { x++; } finally { lock.unlock(); }
}
public int get() {
lock.lock();
try { return x; } finally { lock.unlock(); }
}
}
8.2 LongAdder 抗热点计数
LongAdder adder = new LongAdder();
// 并发线程直接 add,内部分片累加,读时汇总
adder.add(1);
long sum = adder.sum();
8.3 读写锁与条件队列
class RWCache<K,V> {
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private final Map<K,V> map = new HashMap<>();
public V get(K k){
rw.readLock().lock();
try { return map.get(k);} finally { rw.readLock().unlock(); }
}
public void put(K k, V v){
rw.writeLock().lock();
try { map.put(k, v);} finally { rw.writeLock().unlock(); }
}
}
9. 调优与诊断建议
- 观测与基准:
- JFR(Java Flight Recorder)采集阻塞/等待事件(
Java Monitor Blocked、Thread Park)。 - Async-profiler 观察 CPU 自旋热点、
Unsafe.park栈分布。 - 微基准使用 JMH,控制预热、线程数与绑定策略(pin 线程)。
- JFR(Java Flight Recorder)采集阻塞/等待事件(
- 编译与运行参数(按需验证,不做一刀切):
-XX:+UseSpinWait:在部分 CPU 上更友好的自旋指令(如插入 PAUSE)。-XX:PreBlockSpin(旧参数,现代 JDK 多已调整):阻塞前自旋次数。- 公平锁仅在尾延迟敏感时启用,常规吞吐优先用非公平。
- 架构相关注意:
- x86 上一般更容易获得稳定低抖动的 CAS 行为;
- ARM 上注意弱内存序引入的可见性问题,尽量通过
volatile/VarHandle 语义化实现并行算法。
10. 关键要点回顾
- 锁信息存放在对象头的
Mark Word中,随状态复用位段:无锁/轻量级/重量级(偏向锁在新 JDK 中已禁用/移除)。 - CAS 是无锁算法基石,提供原子性与 acquire-release 有序性,但需防范 ABA、活锁与高冲突热点。
- AQS 以 CLH 队列串联失败线程,统一提供独占/共享与条件队列,支撑
ReentrantLock/Semaphore/CountDownLatch等。 - x86 与 ARM 的实现差异主要体现在原子指令与内存屏障上,JVM 屏蔽了差异以兑现 JMM 语义。
- 工程选型优先简单与稳定:能用
synchronized就别过早引入复杂锁;计数热点用LongAdder;高争用尽量结构化拆分,而不是盲目自旋。
附:进一步阅读
- Java Language Specification(JLS)与 Java Memory Model(JMM)章节
- Doug Lea:AQS 源码与论文
- OpenJDK JEP 374:Disable and deprecate biased locking(JDK 15)
- Java Concurrency in Practice(JCIP)
11. JIT 与锁优化:逃逸分析、锁消除、锁粗化
- 逃逸分析(Escape Analysis):JIT 判断对象是否只在当前线程可见。若“未逃逸”,可进行标量替换、栈上分配,并消除不必要的同步。
- 锁消除(Lock Elision):当 JIT 确认同步对象只在单线程上下文使用时,移除
synchronized/轻量级锁操作。 - 锁粗化(Lock Coarsening):当热点循环中频繁短暂加解锁时,JIT 会把多次锁合并到更外层,降低加解锁频率与内存屏障开销。
- 自适应自旋(Adaptive Spinning):JVM 依据历史竞争状况与持锁线程运行状态动态调整自旋时长(结合
park切换),避免盲目自旋或过早阻塞。
工程建议:
- 不要刻意将很多微小操作拆成多个极短的同步块,给 JIT 锁粗化留下空间;
- 热路径上的锁对象尽量局部化,利于逃逸分析与消除。
12. AQS 的 Node 与 waitStatus 详解
AQS 同步队列是双向链表(近似 CLH 的变体),核心节点字段:
static final class Node {
// 等待状态:
// 1 CANCELLED(已取消)
// -1 SIGNAL(前驱释放时需要唤醒本节点)
// -2 CONDITION(在条件队列中)
// -3 PROPAGATE(共享模式传播)
volatile int waitStatus;
volatile Node prev, next;
volatile Thread thread;
// 标记独占/共享模式
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
}
关键机制:
- 失败线程 CAS 入队,前驱的
waitStatus置为SIGNAL,当前驱释放时unpark后继; - 取消(超时/中断)节点会被链路跳过,保持队列健康;
- 共享模式释放时使用
PROPAGATE以继续唤醒后继共享获取者(如Semaphore)。
13. JMM 内存屏障与 happens-before 速查
- 程序次序规则:同一线程内,语句按程序顺序
hb。 - 监视器锁:解锁
hb于后续对同一锁的加锁。 - volatile:对同一变量的写
hb于后续读。 - 线程启动:
Thread.start()之前的操作hb于run()内。 - 线程终止:线程内操作
hb于检测到其终止(join/isAlive返回 false)。 - 中断:
interrupt()先行于被中断线程检测到中断(isInterrupted/InterruptedException)。 - final 字段:构造函数对
final字段的写hb于其他线程看到该对象引用后的读。
内存屏障类别(抽象到硬件):
- LoadLoad, LoadStore, StoreStore, StoreLoad(其中 StoreLoad 最强,常在释放-获取边界上出现)。
14. 常见并发坑与对策
- 双重检查锁(DCL)缺
volatile:实例引用未发布完全可见,务必对实例引用使用volatile或改用静态初始化。 - 锁顺序不一致导致死锁:为多资源加锁规定全局顺序,或使用
tryLock带超时与回退策略。 - 条件丢失与虚假唤醒:
await()后必须用while重新检查条件,不要用if。 - 吞掉中断:捕获
InterruptedException后应恢复中断位或按语义处理,避免“中断失效”。 - 读写混用容器:高并发下不要在无保护的
ArrayList/HashMap上写入;使用并发容器或外部锁。 - 误用
notify():多条件/多消费者模型优先用Condition,或使用notifyAll()的同时配合条件判断。
15. 基准与测试建议
- JMH 微基准:
- 使用
@State控制共享程度; - 充分预热(
@Warmup)与多次迭代(@Measurement); - 使用
Blackhole消除 DCE; - 设定不同的并发度(
@Threads)和绑定策略(避免线程迁移)。
- 使用
- 生产观测:
- 打开 JFR 事件(Monitor Blocked、Thread Park、Java Monitor Wait);
- 使用 async-profiler 结合
-e lock/-e cpu观察竞争与自旋热点; - 采集等待时间分布(P50/P95/P99)而非仅均值。
16. OS 原语映射与实现细节
- Linux:
park/unpark→futex(FUTEX_WAIT/FUTEX_WAKE);内核调度与优先级反转可能影响尾延迟(Java 层无优先级继承)。 - macOS:基于 pthread 互斥量/条件变量;休眠/唤醒路径与时钟源会影响超时精度。
- Windows:现代实现可映射到
WaitOnAddress/Slim Reader-Writer(SRW)等原语。
结论:不同 OS 的调度策略与时钟、唤醒延迟差异会影响 AQS/Monitor 的尾延迟特征,服务端 SLO 设计需留冗余。
17. 选型与落地清单(Checklist)
- 同步原语选择:
- 首选
synchronized,需要可中断/多条件/定时再用ReentrantLock; - 读多写少:
ReentrantReadWriteLock或StampedLock(谨慎使用); - 计数热点:
LongAdder优于单点AtomicLong; - 信号量/门闩:
Semaphore/CountDownLatch,或升级Phaser。
- 首选
- 结构性优化:
- 尽量无共享或分片(sharding);
- 降低持锁时间(IO/阻塞移出临界区);
- 缓存与批处理减少锁竞争频率。
- 诊断运维:
- 监控阻塞时长与争用次数;
- 采集线程栈与锁持有者;
- 压测覆盖极端并发与抖动场景。