第52篇:synchronized深度解析(2026版)
📌系列导航:《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第51篇:线程生命周期与创建方式 |
➡️ 下一篇:第53篇:volatile与JMM(待发布)
🗺️ 本文阅读地图(3 分钟速览)
第51篇学会了创建线程,本篇深入线程安全的核心——synchronized。从对象头到锁升级,从字节码到JVM优化,彻底搞懂这把“万能锁”:
| 模块 | 核心问题 | 一句话回答 |
|---|---|---|
| 三种用法 | synchronized 怎么用? | 修饰实例方法(锁this)、静态方法(锁Class)、代码块(自定义锁对象) |
| 底层原理 | 锁存在哪里? | Java对象头的Mark Word中,64位JVM占8字节 |
| 锁升级 | 性能怎么优化? | 无锁 → 偏向锁 → 轻量级锁 → 重量级锁,逐级升级,不可逆 |
| JVM优化 | 还有哪些优化手段? | 自适应自旋锁、锁消除、锁粗化 |
| 面试最爱问 | 高频考点有哪些? | 见文末 🎤 小节 |
文章目录
- 第52篇:synchronized深度解析(2026版)
- 🗺️ 本文阅读地图(3 分钟速览)
- 一、核心知识点
- 二、生活类比:从“公共厕所”到“VIP 厕所”
- 三、Java 对象布局与 Mark Word
- 四、锁升级全流程
- 4.1 无锁 → 偏向锁
- 4.2 偏向锁 → 轻量级锁
- 4.3 轻量级锁 → 重量级锁
- 4.4 锁升级流程总结
- 五、可重入性原理
- 六、JVM 对 synchronized 的优化
- 6.1 自适应自旋锁
- 6.2 锁消除(Lock Elimination)
- 6.3 锁粗化(Lock Coarsening)
- 七、synchronized vs ReentrantLock
- 八、生产级避坑清单
- 九、面试高频考点
- 🎤 面试官追问陷阱(加分题)
- 十、练习题
- 📊 你的学习进度
- 👉 下一篇文章预告
一、核心知识点
synchronized 是什么?
synchronized 是 Java 内置的互斥同步锁,基于 JVM 实现,保证多线程环境下共享资源的原子性、可见性与有序性,同时具备自动释放锁、可重入的特性。
三种使用方式:
| 使用方式 | 锁对象 | 作用范围 |
|---|---|---|
| 修饰实例方法 | 当前实例this | 同一实例的所有同步方法互斥 |
| 修饰静态方法 | 当前类的Class对象 | 该类所有实例的静态同步方法互斥 |
| 修饰代码块 | 括号内指定的对象 | 锁粒度最细,灵活性最高 |
锁升级路径(JDK 1.6+):
无锁 → 偏向锁 → 轻量级锁 → 重量级锁(单向升级,不可逆)JDK 1.6 之前 synchronized 只有重量级锁,性能差。JDK 1.6 引入锁升级机制,根据竞争程度动态调整锁形态。JDK 15+ 默认禁用偏向锁(-XX:-UseBiasedLocking),但仍需理解其原理。
JVM 优化手段:
- 自适应自旋锁:JVM 自动统计锁竞争情况,决定自旋等待还是阻塞
- 锁消除:JIT 通过逃逸分析,检测到不可能存在竞争时直接移除锁
- 锁粗化:将多个连续的加锁/解锁操作合并为一次大范围加锁
二、生活类比:从“公共厕所”到“VIP 厕所”
synchronized 就像一个公共厕所:
- 厕所只有一间(锁对象),门口挂着一个“占用/空闲”指示牌(Mark Word)。
- 有人进去了,牌子翻到“占用”(获取锁),其他人只能在门口排队(BLOCKED)。
- 里面的人出来,牌子翻回“空闲”(释放锁),排队的人抢着进(锁竞争)。
锁升级就像厕所管理的优化:
- 无锁:厕所空着,谁都能进,但没人进。
- 偏向锁(VIP 厕所):社区给固定住户配了一把专属钥匙(偏向线程 ID),每天都是同一个人用,省去了每次翻牌子的麻烦。
- 轻量级锁(自旋等待):偶尔有客人来,客人不直接排队,而是在门口原地转圈等待(自旋),期望里面的人很快出来。
- 重量级锁(正式排队):客人越来越多,原地转圈太耗体力,社区正式安排保安维持秩序,排队的人进入等待队列(OS 线程阻塞)。
三、Java 对象布局与 Mark Word
synchronized 的锁状态完全存储在 Java 对象头的Mark Word中。
在 64 位 JVM 中,Java 对象的内存布局分为三部分:
- 对象头(Object Header):Mark Word(8 字节)+ Klass Pointer(4 字节,压缩后)
- 实例数据(Instance Data):成员变量
- 对齐填充(Padding):保证对象大小是 8 的倍数
Mark Word 是锁的核心载体:
| 锁状态 | 锁标志位(2bit) | 偏向标志(1bit) | Mark Word 存储内容 |
|---|---|---|---|
| 无锁(不可偏向) | 01 | 0 | 对象 hashCode + 分代年龄 |
| 无锁(可偏向) | 01 | 1 | 空,可存储偏向线程 ID |
| 偏向锁 | 01 | 1 | 偏向线程 ID(54bit)+ epoch(2bit)+ 分代年龄(4bit) |
| 轻量级锁 | 00 | — | 指向线程栈中 Lock Record 的指针(62bit) |
| 重量级锁 | 10 | — | 指向 ObjectMonitor 对象的指针(62bit) |
| GC 标记 | 11 | — | 空 |
💡关键判断:仅靠锁标志位无法区分无锁和偏向锁(都是 01),需配合偏向标志位(biased_lock)一起判断。
四、锁升级全流程
4.1 无锁 → 偏向锁触发条件:对象刚创建,尚未被任何线程锁定。
获取流程:
- 线程尝试获取锁,检查 Mark Word 是否处于可偏向状态(标志位 01 + 偏向标志 0)
- 通过 CAS 将当前线程 ID 写入 Mark Word
- CAS 成功 → 进入偏向锁状态,Mark Word 存储该线程 ID
- CAS 失败 → 说明有其他线程竞争,撤销偏向锁,升级为轻量级锁
偏向锁的核心思想:如果一个线程反复获取同一把锁,每次都要 CAS 操作也是有开销的。偏向锁让该线程“记住”自己,下次直接进入,省去 CAS。
释放:偏向锁不会主动释放。当其他线程尝试获取时,触发偏向撤销。
4.2 偏向锁 → 轻量级锁触发条件:另一个线程尝试获取已被偏向的锁。
升级流程:
- 线程 B 尝试获取锁,发现 Mark Word 中的线程 ID 不是自己
- 触发偏向撤销,JVM 暂停持有锁的线程 A(到达安全点)
- 检查线程 A 是否存活、是否还在同步块中
- 若线程 A 已退出同步块,撤销偏向,恢复为无锁状态,再 CAS 竞争
- 若线程 A 仍在同步块中,升级为轻量级锁
轻量级锁的实现:
- 线程 B 在自己的栈帧中创建一个Lock Record(锁记录)
- 通过 CAS 将 Mark Word 替换为指向 Lock Record 的指针
- CAS 成功 → 线程 B 获取轻量级锁
- CAS 失败 → 说明有竞争,自旋等待
触发条件:自旋次数超过阈值(JVM 自适应),或另一个线程也来竞争。
升级流程:
- 轻量级锁 CAS 替换 Mark Word 失败,说明存在锁竞争
- JVM 将锁膨胀为重量级锁
- Mark Word 替换为指向ObjectMonitor的指针
- 竞争失败的线程进入
_EntryList阻塞等待
ObjectMonitor 核心结构:
ObjectMonitor { _owner // 持有锁的线程 _count // 锁计数器(可重入) _recursions // 重入次数 _EntryList // 等待获取锁的线程队列 _WaitSet // 调用 wait() 的线程队列 }4.4 锁升级流程总结五、可重入性原理
synchronized 是可重入锁——同一个线程可以多次获取同一把锁。
实现原理:
- 每个对象关联一个
ObjectMonitor,内部维护_count计数器 - 线程第一次获取锁:
_owner = 当前线程,_count = 1 - 同一线程再次进入:
_count++(不阻塞) - 退出同步块:
_count-- _count == 0时真正释放锁
publicsynchronizedvoidmethodA(){methodB();// 同一把锁,可重入!}publicsynchronizedvoidmethodB(){// 直接进入,不需要重新竞争}六、JVM 对 synchronized 的优化
6.1 自适应自旋锁- 线程尝试获取轻量级锁失败时,不立即阻塞,而是原地循环等待(自旋)
- JVM 会统计每个锁的自旋成功率,动态调整自旋次数
- 成功率高 → 多自旋一会儿;成功率低 → 少自旋或直接阻塞
- JIT 通过逃逸分析判断锁对象是否只被单个线程访问
- 如果不存在竞争,JVM 直接移除 synchronized
- 示例:
StringBuffer的方法都是 synchronized,但在局部变量中只被单线程访问时,JVM 会消除锁
- 将多个连续的加锁/解锁操作合并成一次大范围加锁
- 减少锁获取/释放的次数,降低开销
- 示例:循环内反复加锁 → JVM 把锁移到循环外
七、synchronized vs ReentrantLock
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层级 | JVM 内置关键字 | JDK 提供的 API 类 |
| 锁释放 | 自动释放(异常也释放) | 需在 finally 中手动 unlock() |
| 可重入性 | ✅ 支持 | ✅ 支持 |
| 公平锁 | ❌ 非公平 | 支持公平/非公平 |
| 中断响应 | ❌ 不可中断 | ✅lockInterruptibly() |
| 超时获取 | ❌ 不支持 | ✅tryLock(timeout) |
| 条件变量 | wait()/notify() | Condition(多个) |
| 性能 | 低竞争时接近,高竞争时略逊 | 高竞争时略优 |
| 锁升级 | ✅ 有(偏向→轻量→重量) | ❌ 无 |
| 适用场景 | 简单同步,低/中竞争 | 高级并发控制,高竞争 |
八、生产级避坑清单
✅ synchronized 使用避坑指南 1. 锁对象不能用 null → 抛 NullPointerException 2. 锁对象不能用基本类型 → 只能用引用类型 3. 不要用 String 常量作为锁对象 → 不同模块可能共用同一字符串常量池,导致意外死锁 4. 不要在锁内执行耗时操作 → 长时间持有锁,阻塞其他线程 5. 注意锁粒度 → 能锁代码块就别锁整个方法 6. wait() 必须在 synchronized 块中调用 → 否则抛 IllegalMonitorStateException 7. 虚拟线程中慎用 synchronized → 会导致 Pinning(固定在载体线程),建议用 ReentrantLock九、面试高频考点
Q1:synchronized 的三种使用方式?锁对象分别是什么?
① 修饰实例方法 → 锁当前实例
this;② 修饰静态方法 → 锁当前类的Class对象;③ 修饰代码块 → 锁括号内指定的对象。
Q2:synchronized 的锁存在哪里?
存在 Java 对象头的Mark Word中。64 位 JVM 中 Mark Word 占 8 字节,存储锁状态、线程 ID、hashCode 等信息。
Q3:synchronized 锁升级的过程?
从低到高依次为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,单向升级不可逆。JDK 1.6 引入,JDK 15+ 默认禁用偏向锁。
Q4:偏向锁、轻量级锁、重量级锁分别解决什么问题?
偏向锁针对无竞争场景,避免 CAS 开销;轻量级锁针对轻度竞争,通过自旋避免线程阻塞;重量级锁针对重度竞争,通过 OS 互斥量阻塞线程。
Q5:synchronized 的可重入性是如何实现的?
每个对象关联的
ObjectMonitor维护_count计数器。同一线程重复获取时_count++,退出时_count--,归零时释放锁。
Q6:JVM 对 synchronized 做了哪些优化?
①锁升级(偏向→轻量→重量);②自适应自旋锁(动态调整自旋次数);③锁消除(逃逸分析移除无竞争锁);④锁粗化(合并连续加锁/解锁)。
🎤 面试官追问陷阱(加分题)
追问1:“JDK 15 为什么默认禁用偏向锁?”
👉 偏向锁的维护成本(撤销、安全点)在大多数应用场景下超过了其带来的收益。现代应用通常有多个线程竞争,偏向锁优势不明显。可通过
-XX:+UseBiasedLocking重新启用。
追问2:“轻量级锁一定比重量级锁快吗?”
👉不一定。轻量级锁通过自旋等待,适用于锁持有时间短、竞争不激烈的场景。如果锁持有时间长或竞争激烈,自旋会浪费 CPU,此时重量级锁让线程阻塞反而更高效。JVM 的自适应自旋会根据历史统计动态决策。
追问3:“虚拟线程中为什么慎用 synchronized?”
👉 虚拟线程在
synchronized块中会被固定在载体线程上(Pinning),无法被卸载,失去轻量级优势。高并发场景建议用ReentrantLock替代。
十、练习题
分析题:以下代码中,两个线程分别调用
methodA()和methodB()会互相阻塞吗?为什么?publicclassTest{publicsynchronizedvoidmethodA(){...}publicstaticsynchronizedvoidmethodB(){...}}💡 思路:
methodA()锁的是实例this,methodB()锁的是Test.class,两者互不影响。代码题:用 JOL 工具打印一个普通对象的对象头布局,观察无锁状态下的 Mark Word。
场景设计:某系统使用 synchronized 保护共享资源,并发量高时性能急剧下降。分析可能的原因和优化方案。
💡 思路:可能是锁粒度太大(锁整个方法)或锁持有时间过长。优化方案:缩小锁粒度(锁代码块)、减少锁内操作、考虑使用 ReentrantLock 或 ConcurrentHashMap。
📊 你的学习进度
- 当前:第52篇 / 共108篇 ·进阶篇:并发编程与JUC详解(第51~60篇)
- ✅ 已完成:基础篇44篇 + 第45~52篇
- 📖 正在学:第52篇
- ⏳ 待学习:第53~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
👉 下一篇文章预告
🚀下一篇:《第53篇:volatile与JMM》
内容简介:Java 内存模型(JMM)、可见性、有序性、禁止指令重排、happens-before 原则、volatile 与 synchronized 的区别。
👉多线程专题持续深入,拿下 JMM 核心!
📌《Java 100 天进阶之路 | 从入门到上岗就业》每天一篇,建议收藏 + 关注,一起100天拿offer! 👉 点击关注我,更新后第一时间收到推送!