高频八股 · Java 并发编程(4):synchronized 与锁升级,一次讲透
写在前面
上一期讲 CAS,它是"无锁"的思路:不加锁,靠自旋重试。
但无锁只适合冲突少的场景。一旦冲突激烈、临界区又大,还是得老老实实加锁。
这一期就聊 Java 里最经典的锁——synchronized。
一、synchronized 锁的到底是谁?
synchronized 有三种用法,锁的对象完全不一样,这是第一个容易混的点:
// 1. 修饰实例方法:锁的是「当前实例 this」
public synchronized void method() { }
// 2. 修饰静态方法:锁的是「当前类的 Class 对象」
public static synchronized void staticMethod() { }
// 3. 修饰代码块:锁的是「括号里的对象」
public void block() {
synchronized (this) { } // 锁 this
synchronized (Singleton.class) { } // 锁 Class
}
一句话记牢:
- 实例方法 → 锁
this; - 静态方法 → 锁
Xxx.class; - 代码块 → 锁括号里的那个对象。
注:
synchronized是可重入的,同一个线程再次进入同一把锁,不会被自己阻塞。
二、底层原理:对象头 + Monitor
① 对象头里的 Mark Word
每个 Java 对象在内存里都有一段"对象头",其中 Mark Word 会记录运行时的状态:哈希码、GC 分代年龄、锁状态标志、偏向线程 ID 等。
锁标志位(低 2 位)大概是这么分的:
| 标志位 | 状态 |
|---|---|
01 | 无锁 / 偏向锁 |
00 | 轻量级锁 |
10 | 重量级锁 |
11 | GC 标记 |
② Monitor(管程)
每个对象都关联一个 Monitor(C++ 里的 ObjectMonitor),它内部维护:
_owner:当前持有锁的线程;_entryList:在门外排队等锁的线程;_waitSet:调用wait()后进入等待的线程;_recursions:重入次数。
③ 字节码层面
- 同步代码块:编译后出现
monitorenter/monitorexit指令; - 同步方法:方法上打一个
ACC_SYNCHRONIZED标志。
无论哪种,最终都是围绕 Monitor 的
_owner来抢锁。
三、锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
这是 synchronized 的核心考点。为什么要升级?因为不同竞争程度,用最合适的锁最划算——能用轻的就不用重的。
3.1 偏向锁(Bias)
场景:只有一个线程反复进入同步块,压根没竞争。
做法:第一次加锁时,用 CAS 把自己的线程 ID 写进 Mark Word。之后这个线程再来,只要检查 Mark Word 里的 ID 是自己,就直接放行——连 CAS 都省了。
代价:一旦有别的线程来抢,就得撤销偏向锁,这个操作比较贵。
⚠️ 补充个新变化:JDK 15 起,偏向锁已默认禁用(JEP 374)。原因就是维护成本高、实际收益有限。面试说出来会显得你跟得上版本。
3.2 轻量级锁
场景:有竞争,但很轻(线程交替执行,几乎不重叠)。
做法:
- 把 Mark Word 复制到当前线程栈帧里的 Lock Record;
- 用 CAS 把对象头的 Mark Word 指向这个 Lock Record;
- 成功就拿到锁;失败就自旋重试几次。
自旋还是失败?那就只能膨胀了。
3.3 重量级锁
场景:竞争激烈,自旋也抢不到。
做法:锁膨胀成重量级锁,抢不到锁的线程进入 Monitor 的 _entryList 阻塞,需要操作系统介入调度。
代价:涉及用户态 → 内核态切换,开销最大。
3.4 升级链条(记住这张图)
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
关键结论:锁只能升级,一般不能降级。(一旦膨胀成重量级,就很难退回去了)
四、锁优化:JVM 帮你偷偷做的事
① 锁消除
JIT 通过逃逸分析,发现某个锁对象根本不会逃逸出当前方法(比如方法内 new 的 StringBuffer),就直接把锁去掉,压根不加。
② 锁粗化
如果你在循环里反复加锁解锁:
for (int i = 0; i < 1000; i++) {
synchronized (lock) { doSomething(); }
}
JVM 会把它合并成一次大锁,避免频繁加解锁的开销。
③ 自适应自旋
自旋次数不是固定的,JVM 会根据上次自旋是否成功动态调整——成功多就多转几圈,失败多就少转。
五、synchronized vs ReentrantLock
这张表是为下一期铺路的,先有个印象:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层次 | JVM 层(Monitor) | JDK 层(AQS) |
| 锁释放 | 自动 | 手动 unlock()(必须放 finally) |
| 可中断 | ❌ | ✅ lockInterruptibly() |
| 公平锁 | 只能非公平 | 可选公平 / 非公平 |
| 条件变量 | 1 个(wait/notify) | 多个 Condition |
| 尝试获取 | ❌ | ✅ tryLock() |
一句话:简单场景用 synchronized(省心、不会忘解锁);需要超时、可中断、多条件、公平锁,才用 ReentrantLock。
六、高频面试题速答
Q1:synchronized 的三种用法分别锁什么?
实例方法锁 this、静态方法锁 Class 对象、代码块锁括号里的对象。
Q2:synchronized 的底层原理?
对象头 Mark Word + Monitor。代码块靠 monitorenter/monitorexit,方法靠 ACC_SYNCHRONIZED 标志。
Q3:锁升级的过程?
无锁 → 偏向锁(单线程,CAS 写线程 ID)→ 轻量级锁(有竞争,CAS 指向 Lock Record + 自旋)→ 重量级锁(竞争激烈,进入 Monitor 阻塞)。只能升不能降。
Q4:什么是锁消除和锁粗化?
锁消除:逃逸分析发现锁对象不逃逸,直接去掉锁;锁粗化:把循环里多次加解锁合并成一次。
Q5:偏向锁为什么被废弃?
撤销成本高、收益有限,JDK 15 起默认禁用。
Q6:synchronized 和 ReentrantLock 的区别?
见上表——核心是:synchronized 自动释放、简单;ReentrantLock 手动释放,但支持可中断、公平锁、多条件、tryLock。
七、一句话总结
synchronized 锁的是对象,底层靠 Mark Word + Monitor;竞争越激烈,锁就越"重"——能轻则轻,这就是锁升级的意义。
(本系列持续更新中,共 10 期,敬请关注~)
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2401_88601046/article/details/168343077




