Java 锁机制:Synchronized 原理、锁升级与 Lock 对比
Java 锁机制:Synchronized 原理、锁升级与 Lock 对比
本文从锁策略的分类体系讲起,逐层深入到 synchronized 的底层实现,最后对比 synchronized 和 Lock。
文章目录
一、先建坐标系:Java 中的锁策略分类
在深入源码之前,先理解「锁有哪些分类维度」——这些是锁的设计策略,不是具体实现。
1. 乐观锁 vs 悲观锁
如何看待「冲突」?
悲观锁 乐观锁
"肯定有人跟我抢" "大概率没人抢"
先加锁,再操作 先操作,提交时检查
实现:synchronized, Lock 实现:CAS, 版本号
适合:写多读少 适合:读多写少
// 悲观锁 —— 先加锁
synchronized (obj) {
count++; // 我锁住了,谁也进不来
}
// 乐观锁 —— 先改再看有没有冲突
atomicInteger.incrementAndGet(); // CAS 循环,冲突就重试
乐观锁的核心:CAS(Compare And Swap)
CAS(V, A, B):
if (内存地址 V 的当前值 == 预期值 A)
V = B; // 更新成功
return true;
else
return false; // 被别人改了,重试
ABA 问题:线程 1 读 V=A → 线程 2 改 V=A→B→A → 线程 1 CAS 发现还是 A,以为没变。解决:加版本号(AtomicStampedReference)。
2. 公平锁 vs 非公平锁
┌── 公平锁:先来后到,排队
锁 ────┤
└── 非公平锁:来了就抢,抢不到再排队
| 公平锁 | 非公平锁 | |
|---|---|---|
| 获取顺序 | 按申请顺序(FIFO) | 新来的可以先抢 |
| 性能 | 较低(频繁线程切换) | 更高(减少上下文切换) |
| 实现 | new ReentrantLock(true) |
new ReentrantLock()(默认) |
| 饥饿 | 不会 | 可能——有些线程永远抢不到 |
synchronized 天然是非公平锁。为什么默认非公平?因为公平锁每次都要从队列里唤醒一个线程,上下文切换成本远高于「刚释放的锁被正在运行的线程直接抢到」。
3. 可重入锁 vs 不可重入锁
可重入:同一个线程可以多次获取同一把锁。Java 中 synchronized 和 ReentrantLock 都是可重入的。
public synchronized void methodA() {
methodB(); // methodB 也需要同一把锁——可重入的话不会死锁
}
public synchronized void methodB() {
// 如果没有重入机制,这里就死锁了
}
原理:每个锁维护一个 持有者线程 ID + 重入计数器。同一线程每进一次计数器 +1,每退一次 -1,归零时真正释放锁。
4. 独占锁 vs 共享锁(读写锁)
| 独占锁(互斥锁) | 共享锁 | |
|---|---|---|
| 读-读 | 互斥 | 可并发 |
| 读-写 | 互斥 | 互斥 |
| 写-写 | 互斥 | 互斥 |
| 代表 | synchronized, ReentrantLock | ReentrantReadWriteLock 的读锁 |
其实还有读写互斥的一种细分:
- 写锁(独占锁 / X 锁):别人不能读也不能写
- 读锁(共享锁 / S 锁):别人能读但不能写
5. 自旋锁 vs 阻塞锁
自旋锁:抢不到锁 → while 循环一直等(不释放 CPU)
适合:锁持有时间极短
阻塞锁:抢不到锁 → 挂起线程,让出 CPU
适合:锁持有时间长
代价:线程切换本身的开销(用户态 ↔ 内核态)
Java 的 synchronized 做了聪明的事:先自旋一会儿(自适应自旋),抢不到再阻塞。
二、锁策略速查表
| 分类维度 | 选项 A | 选项 B | synchronized 属于 |
|---|---|---|---|
| 竞争态度 | 悲观锁(先锁) | 乐观锁(CAS 重试) | 悲观锁 |
| 排队策略 | 公平锁(FIFO) | 非公平锁(可抢) | 非公平锁 |
| 重入性 | 可重入 | 不可重入 | 可重入 |
| 共享性 | 独占锁 | 共享锁 | 独占锁 |
| 等待方式 | 自旋锁 | 阻塞锁 | 自适应(先自旋后阻塞) |
| 粒度 | 对象锁 | 类锁 | 都支持 |
三、synchronized 底层原理:对象头与 Monitor
Java 对象的内存布局
任何一个 Java 对象,在堆内存中长这样:
┌─────────────────────────────────────────────────┐
│ 对象头 (Header) │
│ ┌──────────────────────┬──────────────────────┐ │
│ │ Mark Word (64 位) │ Klass Pointer (64 位) │
│ │ 锁状态/GC 分代/hash │ 指向类元数据 │
│ └──────────────────────┴──────────────────────┘ │
├─────────────────────────────────────────────────┤
│ 实例数据 (Instance Data) │
│ 成员变量:int, String, 引用... │
├─────────────────────────────────────────────────┤
│ 对齐填充 (Padding) │
│ 保证对象大小是 8 字节的倍数(64 位 JVM) │
└─────────────────────────────────────────────────┘
Mark Word 是核心——它存储了对象的 hash、GC 年龄、锁状态。同一块内存,不同锁状态下存储的内容完全不同:
Mark Word 在不同锁状态下的结构
无锁状态:
┌──────────────────────┬───────┐
│ unused (25bit) │ hash │ age │ 0 │ 01 │
│ 未使用 │ 31bit │ 4bit│ │ │
└──────────────────────┴───────┘
↑ 锁标志位:001 = 无锁
偏向锁:
┌──────────────────────┬───────┐
│ 线程 ID (54bit) │ epoch │ age │ 1 │ 01 │
│ 持有锁的线程 │ 2bit │ 4bit│ │ │
└──────────────────────┴───────┘
↑ 锁标志位:101 = 偏向锁
轻量级锁:
┌────────────────────────────────┐
│ 指向线程栈中 Lock Record 的指针 │ ... │ 00 │
│ (62bit) │ │ │
└────────────────────────────────┘
↑ 锁标志位:00 = 轻量级锁
重量级锁:
┌────────────────────────────────┐
│ 指向 Monitor 对象的指针 │ ... │ 10 │
│ (62bit) │ │ │
└────────────────────────────────┘
↑ 锁标志位:10 = 重量级锁
锁标志位只有 2 位(最后两位),这 2 位决定了整把锁的状态机如何跳转。
Monitor(管程)——重量级锁的核心
每个 Java 对象在 JVM 层面关联一个 Monitor 对象(C++ 的 ObjectMonitor):
┌──────────────────────────────┐
│ ObjectMonitor │
│ │
│ _owner → 当前持锁线程 │
│ _EntryList → 抢锁失败,阻塞在这 │
│ _WaitSet → wait() 方法后进入 │
│ _recursions → 重入计数器 │
│ _count → 锁计数器 │
└──────────────────────────────┘
// synchronized 代码块的字节码
public void method() {
synchronized (this) { // ← monitorenter 指令
// 临界区
} // ← monitorexit 指令
}
// 注意:编译器会自动加一个异常处理的 monitorexit,
// 保证即使抛异常也能释放锁
synchronized 方法:方法标志位中设置 ACC_SYNCHRONIZED,JVM 自动处理。
四、锁升级:synchronized 的性能优化之路
JDK 1.6 之前,synchronized 直接是重量级锁(每次加锁都要系统调用),性能极差。锁升级是 JDK 1.6 引入的关键优化——锁只会在竞争升级时才变重。
无锁
│
▼ (线程 A 第一次获取锁)
偏向锁 ← 记录线程 A 的 ID
│ A 再次获取:比较 ID,相同则直接进入
│
▼ (线程 B 也来抢这把锁)
轻量级锁 ← CAS 自旋抢
│ 抢到了 → 继续执行(无阻塞)
│ 抢不到 → 自适应自旋
│
▼ (自旋超时 / 竞争太激烈 / 调用 wait)
重量级锁 ← Monitor,阻塞等待,OS 内核调度
(不可逆——一旦升级到重量级,不会再降回来)
偏向锁(Biased Locking)
思想:「这把锁大概率一直只被一个线程用,那就干脆让它独占」。
线程 A 第一次获取锁:
→ Mark Word 记录线程 A 的 ID
→ 偏向锁标志位置为 1
线程 A 再次获取:
→ 检查 Mark Word 中的线程 ID == 自己?→ 是,什么都不做,直接进入
→ 零开销!没有 CAS,没有系统调用
线程 B 也来获取:
→ 线程 ID 不匹配 → 撤销偏向锁(STW 安全点操作)
→ 升级为轻量级锁
JDK 15 默认关闭了偏向锁,JDK 21 直接废弃了。原因是:现代高并发场景下,一把锁被单个线程独占的情况越来越少,偏向锁的撤销成本反而拖累了性能。
轻量级锁(Lightweight Locking)
思想:「锁有竞争,但大家都在自旋,不会长期阻塞」。
流程:
1. 线程在当前栈帧中分配 Lock Record
2. 把对象的 Mark Word 拷贝到 Lock Record
3. CAS 尝试把对象的 Mark Word 替换为「指向 Lock Record 的指针」
│
├── CAS 成功 → 轻量级锁获取成功(锁标志位 00)
│
└── CAS 失败 → 检查指针是否指向自己的 Lock Record
├── 是 → 重入(再来一个 Lock Record)
└── 否 → 自旋等待
自旋超时 → 升级为重量级锁
自适应自旋:JVM 不固定自旋次数——根据「上次这个锁自旋了多久才获取到」动态调整。上次自旋成功了,这次多等一会儿;上次失败了,这次早点升级。
重量级锁(Heavyweight Locking)
思想:「抢不过了,交给 OS 吧」。
升级到重量级锁后:
- 对象的 Mark Word 指向 Monitor
- 没拿到锁的线程进入 Monitor 的 EntryList,被挂起(阻塞态)
- 调用
wait()的线程进入 WaitSet,等待notify() - 锁释放后,OS 从 EntryList 中选一个线程唤醒
代价:用户态 ↔ 内核态切换,每次 ~ 几微秒。
五、synchronized vs Lock:一张表说清楚
| 维度 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 关键字/类 | Java 关键字,JVM 层面实现 | java.util.concurrent.locks 的接口 |
| 加锁释放 | 自动——代码块结束/异常自动释放 | 手动——lock() → try...finally → unlock() |
| 可中断 | ❌ 不可中断,等不到就一直等 | ✅ lockInterruptibly(),可响应中断 |
| 超时获取 | ❌ 不支持 | ✅ tryLock(time, unit) |
| 公平性 | ❌ 非公平 | ✅ 可选:new ReentrantLock(true) |
| 条件变量 | wait/notify/notifyAll,单一等待队列 |
Condition,可多个条件队列 |
| 性能 | JDK 6+ 经过锁升级优化,不差 | 底层也是 AQS + CAS,与轻量级锁场景持平 |
| 读/写分离 | ❌ 独占锁 | ✅ ReentrantReadWriteLock |
| 底层实现 | Monitor(C++ ObjectMonitor) | AQS(AbstractQueuedSynchronizer) + CAS |
| 适用场景 | 简单同步,锁持有时间短 | 需要高级功能:超时、中断、条件变量、公平性 |
核心原则
能用 synchronized 就用 synchronized,需要高级功能再上 Lock。
原因:synchronized 自动释放、锁升级优化、JVM 甚至可以对它做「锁消除」「锁粗化」等编译优化——这些都是 Lock 享受不到的。
六、AQS:Lock 背后的魔法
ReentrantLock 不是自己实现的锁逻辑——所有 Java 并发工具(ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock、CyclicBarrier…)背后都是同一个人:AQS(AbstractQueuedSynchronizer)。
AQS 的设计
AQS 是一个模板类,内部维护:
┌── state (volatile int) ── 锁状态:0 = 未占用,>0 = 已占用
│
└── CLH 队列(双向链表)── 抢不到锁的线程排队等待
│
┌───┴───┐ ┌───┐ ┌───┐
│ Head │ → │ T1│ → │ T2│ → ... (FIFO,可配置公平/非公平)
└───────┘ └───┘ └───┘
ReentrantLock 加锁流程
// ReentrantLock.lock() 内部逻辑(简化)
void lock() {
// 1. 先 CAS 抢一把(非公平锁的精髓)
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(currentThread); // 抢到了
return;
}
// 2. 没抢到,进入 acquire
acquire(1);
}
void acquire(int arg) {
// 3. tryAcquire 再抢一次(可能重入)
if (!tryAcquire(arg))
// 4. 还是失败 → 入队,park 挂起
acquireQueued(addWaiter(Node.EXCLUSIVE), arg);
}
Condition:比 wait/notify 更强大
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// 生产者
lock.lock();
try {
while (queue.isFull()) {
notFull.await(); // 队列满了,等消费者通知
}
queue.add(item);
notEmpty.signal(); // 通知消费者「有货了」
} finally {
lock.unlock();
}
// 消费者
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空了,等生产者通知
}
queue.take();
notFull.signal(); // 通知生产者「有空位了」
} finally {
lock.unlock();
}
synchronized 的
wait/notify只能用一个等待队列,而 Lock 的 Condition 可以创建多个,实现精确唤醒——这正是ArrayBlockingQueue内部的设计。
七、JVM 对 synchronized 的「作弊」优化
除了锁升级,JVM 还会在编译期做这些优化:
锁消除(Lock Elimination)
// 你写的代码
public String concat() {
StringBuffer sb = new StringBuffer(); // StringBuffer 是线程安全的
sb.append("a").append("b").append("c");
return sb.toString();
}
// sb 是局部变量,不可能被其他线程引用
// JIT 编译器直接删除 append 方法上的 synchronized
锁粗化(Lock Coarsening)
// 你写的代码
for (int i = 0; i < 100; i++) {
synchronized (obj) { // 每次循环加锁释锁,100 次!
doSomething();
}
}
// JIT 优化后(锁粗化)
synchronized (obj) { // 一次锁住,执行 100 次
for (int i = 0; i < 100; i++) {
doSomething();
}
}
八、实战选择清单
| 场景 | 推荐锁 |
|---|---|
| 简单同步,短临界区 | synchronized —— 简单、安全、有优化 |
| 需要超时放弃 | ReentrantLock.tryLock(time, unit) |
| 需要可中断的锁等待 | ReentrantLock.lockInterruptibly() |
| 读多写少 | ReentrantReadWriteLock 或 StampedLock |
| 需要公平排队 | new ReentrantLock(true) |
| 生产者-消费者 | Lock + Condition(多条件队列) |
| 需要尝试获取,失败不等待 | ReentrantLock.tryLock() |
| 大量线程竞争 | 两者都可以;超高竞争下 Lock 略优(条件变量优势) |
九、一张脑图收尾
Java 锁体系
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
锁策略(设计) synchronized(实现) Lock 框架(实现)
│ │ │
乐观 vs 悲观 关键字 接口
公平 vs 非公平 锁升级(1.6+) ReentrantLock
可重入 vs 不可重入 │ ReentrantReadWriteLock
独占 vs 共享 │ StampedLock
自旋 vs 阻塞 │ │
│ ┌───┼───┐ │
│ │ │ │ ▼
▼ ▼ ▼ ▼ AQS
CAS / 版本号 无锁→偏向→轻量→重量 CLH 队列 + state
(底层原子操作) (不可逆) (模板方法模式)
│
┌────┴────┐
▼ ▼
公平锁 非公平锁
一句话总结
- 锁策略:悲观/乐观、公平/非公平、可重入/不可重入——这是锁的「个性」维度
- synchronized:JVM 内置,自动释放,从偏向锁→轻量级锁→重量级锁逐级升级,还有锁消除/锁粗化等 JIT 优化
- Lock:API 层面,手动释放,但支持超时、中断、公平性、多条件队列——功能更强但责任也更大
- 选锁原则:先考虑 synchronized,不够用再上 Lock。优化是 JVM 的事,正确性是自己的事
更多推荐



所有评论(0)