Java 中的锁可以从多个维度分类,但核心演进脉络集中在 synchronized从 "纯重量级锁"到"锁升级体系"的优化,以及 JUC 显式锁体系的成熟。下面按分类、演进、原理三层展开。
一、Java 锁的宏观分类
1. 按实现层面
| 类型 | 代表 | 控制方 |
|---|---|---|
| 隐式锁 | synchronized | JVM 自动管理(进入/退出 monitorenter/monitorexit) |
| 显式锁 | ReentrantLock、ReadWriteLock | 程序员手动lock()/unlock() |
2. 按竞争策略
| 类型 | 思想 | 代表 |
|---|---|---|
| 悲观锁 | 先加锁,再操作 | synchronized、ReentrantLock |
| 乐观锁 | 先操作,提交时 CAS 检查 | AtomicInteger、LongAdder |
3. 按特性
- 公平/非公平:是否按请求顺序分配锁
- 可重入/不可重入:同一线程能否多次获取同一把锁
- 共享/独占:读锁共享、写锁独占
- 自旋/阻塞:竞争时 CPU 空转等待 vs 线程挂起
4. 按状态(synchronized 核心)
演进的主线:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
二、锁的演进史:synchronized 的四代优化
1. JDK 1.0 ~ 1.5:纯重量级锁(性能黑洞)
早期 synchronized 直接调用操作系统mutex(互斥量):
- 线程阻塞 = 用户态 → 内核态切换
- 线程唤醒 = 内核态 → 用户态切换
- 即使单线程反复进入,也要走完整内核流程
当时 Java 并发性能被严重诟病,直到 Doug Lea 推出 JUC(java.util.concurrent) 包,用 ReentrantLock + CAS 绕过 JVM 锁。
2. JDK 1.6:锁升级体系(JVM 团队反击)
JVM 团队发现:大多数锁不仅不存在多线程竞争,而且总是由同一线程多次获得。于是引入 锁升级(Lock Coarsening/Elimination 之外的核心优化)
① 无锁(Lock-Free)
- 对象初始状态,或显式使用 Atomic 类
- 依赖 CPU 的 cmpxchg(CAS)指令
- Mark Word 标志:…| age:4 | biased_lock:0 | lock:01
② 偏向锁(Biased Locking)
- 场景:只有一个线程访问同步块
- 原理:第一次获取锁时,用 CAS 把线程 ID 写入对象头的 Mark Word。之后该线程进入同步块,只需检查 Mark Word 里的线程 ID 是否是自己,无需任何原子操作。
- 撤销成本:一旦有第二个线程尝试获取,必须在安全点(STW)撤销偏向锁,这个代价不小。
- 现状:JDK 15 废弃(JEP 374),JDK 18 默认关闭。现代硬件上收益变小,撤销成本反而成为负担。
③ 轻量级锁(Lightweight Locking)
- 场景:多线程交替执行(竞争不激烈,没有同时抢)
- 原理:线程在自己的栈帧中创建 Lock Record,然后用 CAS 把对象头的 Mark Word 替换为指向该 Lock Record 的指针。
- 成功:获得锁
- 失败:自旋(默认 10 次,JDK 1.6 后引入自适应自旋:根据历史成功率动态调整次数)
- 优点:线程不阻塞,避免内核态切换
- 缺点:自旋消耗 CPU,竞争激烈时自旋就是浪费
④ 重量级锁(Heavyweight Locking)
- 场景:竞争激烈,自旋失败
- 原理:锁膨胀(inflate)为 Monitor(ObjectMonitor)
- 对象头 Mark Word 指向 ObjectMonitor 地址
- 未获得锁的线程进入 _EntryList 阻塞
- 持有锁的线程执行完后,从 _EntryList 唤醒一个竞争线程
- 开销:内核态切换,但适合长时间持有锁的场景
3. JDK 1.6 另外两项编译优化
| 优化 | 原理 | 效果 |
|---|---|---|
| 锁消除(Lock Elimination) | 逃逸分析发现锁对象不会被其他线程访问,直接去掉同步 | 局部变量StringBuffer的同步常被消除 |
| 锁粗化(Lock Coarsening) | 相邻的同步块合并为一个更大的同步块 | 减少反复加锁/解锁开销 |
三、Mark Word 结构:锁状态的载体
对象头中的Mark Word是锁状态的存储载体。以64 位 JVM为例:
| 锁状态 | 62 bits | biased_lock (1 bit) | lock (2 bits) |
|---|---|---|---|
| 无锁 | unused:25 + hash:31 + unused:1 + age:4 | 0 | 01 |
| 偏向锁 | thread:54 + epoch:2 + unused:1 + age:4 | 1 | 101 |
| 轻量级锁 | ptr_to_lock_record:62 | - | 00 |
| 重量级锁 | ptr_to_monitor:62 | - | 10 |
| GC 标记 | ptr_to_cms_promoted_obj:62 | - | 11 |
lock 字段(2 bits)+ biased_lock 字段(1 bit)共同标识当前锁状态。
四、锁升级完整流程图
对象创建 │ ▼ ┌─────────────┐ │ 无锁状态 │◄────────────────────────┐ │ (匿名可偏向) │ │ └──────┬──────┘ │ │ │ ▼ │ 线程A尝试获取 │ │ │ ▼ │ ┌─────────────┐ 线程A再次进入 │ │ 偏向锁状态 │◄──────────────────┐ │ │ Mark Word记录 │ │ │ │ 线程A ID │ │ │ └──────┬──────┘ │ │ │ │ │ ▼ 线程B尝试获取 │ │ 检查epoch/撤销 │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ 轻量级锁 │ CAS成功 │ │ │ 自旋竞争 │───────────────────┘ │ └──────┬──────┘ │ │ │ ▼ 自旋失败/竞争激烈 │ 锁膨胀(inflate) │ │ │ ▼ │ ┌─────────────┐ 锁释放后 │ │ 重量级锁 │────────────────────────┘ │ ObjectMonitor│ └─────────────┘关键规则:
- 锁升级是单向的:偏向锁 → 轻量级锁 → 重量级锁,不能降级
- 重量级锁释放后,对象回到无锁状态,下次竞争重新走升级流程
- 偏向锁撤销后,可能直接升级为轻量级锁,也可能先回到无锁
五、显式锁体系:JUC 的补足
synchronized 的升级体系是 JVM 自动优化的,但灵活性不足。JUC 提供了更精细的控制:
1. ReentrantLock(基于 AQS)
ReentrantLocklock=newReentrantLock(true);// true=公平锁lock.lock();try{// 临界区}finally{lock.unlock();}- 底层是 AQS(AbstractQueuedSynchronizer),通过 volatile int state + CAS + 双向链表管理队列
- 支持公平/非公平、可中断(lockInterruptibly)、超时(tryLock(timeout))、多条件变量(Condition)
2. ReadWriteLock
- 读锁:共享锁,多个线程可同时读
- 写锁:独占锁,写时阻塞读写
- 适合读多写少场景
3. StampedLock(JDK 8)
- 引入乐观读锁:先读,提交时验证版本号,未被写则成功,被写了再升级为悲观读
- 性能比 ReadWriteLock 更高,但使用复杂
六、对比总结
| 维度 | synchronized | ReentrantLock | CAS(无锁) |
|---|---|---|---|
| 实现层 | JVM(字节码monitorenter) | API(AQS) | CPU 指令 |
| 锁升级 | ✅ 支持(偏向→轻量→重量) | ❌ 直接 AQS 队列 | 无锁,无升级 |
| 公平性 | 非公平 | 可选公平/非公平 | - |
| 可中断 | ❌ 不支持 | ✅lockInterruptibly | - |
| 条件队列 | 1 个(wait/notify) | 多个Condition | - |
| 性能 | JDK 1.6 后接近 ReentrantLock | 略优(极端竞争) | 无竞争时最优 |
| 使用难度 | 低(自动释放) | 高(需finally unlock) | 高(ABA 问题) |
七、演进时间线
| 版本 | 锁相关演进 |
|---|---|
| JDK 1.0 | synchronized纯重量级锁,性能差 |
| JDK 1.2 | 引入Lock接口雏形,但 synchronized 未优化 |
| JDK 1.5 | JUC 包正式发布(ReentrantLock、Atomic、AQS) |
| JDK 1.6 | 锁升级(偏向/轻量/重量)、锁消除、锁粗化、自适应自旋 |
| JDK 1.7 | G1 GC,继续优化同步性能 |
| JDK 1.8 | 引入StampedLock、LongAdder(分段 CAS) |
| JDK 15 | 废弃偏向锁(JEP 374),现代并发模型下收益为负 |
| JDK 18 | 默认关闭偏向锁,未来可能彻底移除 |
八、总结
Java 锁的演进本质是"能不动内核就不动内核":从早期直接调用操作系统 mutex(重量级),到发现大多数锁无竞争后引入偏向锁(零开销)、轻量级锁(CAS 自旋),再到 JUC 用 AQS 把排队逻辑搬到用户态。最终趋势是偏向锁被淘汰,轻量级锁 + CAS + AQS成为主流。