第039篇 synchronized 原理:对象头、锁升级与重量级锁

简介: `synchronized` 底层依托对象头 Mark Word 与 `monitorenter` 指令,锁随竞争升级:无锁 → 偏向锁(JDK15 已废弃)→ 轻量级锁(CAS 自旋)→ 重量级锁(ObjectMonitor + 内核阻塞)。`wait` 必在同步块内调用,确保条件检查与等待原子性。

synchronized 这题的分水岭特别清楚:能答出"对象头 Mark Word + monitorenter"的人不少,能讲清"锁升级的四个阶段、偏向锁在 JDK 15 被废弃、以及为什么 wait 必须在锁内调用"的人很少。面试官问的就是后面这层。理解了升级路径与锁竞争的真实代价,这道题才算拿下来。

先把结论放在前面:synchronized 的锁信息记录在对象头的 Mark Word 里,字节码层面由 monitorenter 与 monitorexit 指令实现,进入时若对象未被锁定则把锁记录写进 Mark Word 并指向一个 ObjectMonitor;同一线程重入时用重入计数递增,退出时递减归零才真正释放。 竞争加剧时锁会逐步膨胀:无竞争时靠 Mark Word 标记即可(偏向),轻度竞争用 CAS 自旋(轻量级),自旋失败转内核互斥锁(重量级)。

机制拆解

讲清升级路径这条主线。无竞争时,JVM 可以让 Mark Word 记录"锁偏向于某线程",该线程再进入只需比较线程 ID,几乎零成本——不过这条路径在 JDK 15 已被废弃并默认关闭,因为偏向锁在多线程竞争场景收益有限、却持续占用额外判断。轻度竞争下(预计很快能拿到锁),改用轻量级锁:在线程栈上生成 Lock Record,用 CAS 把 Mark Word 指向它,自旋若干次尝试加锁。 自旋失败则膨胀为重量级锁,线程被挂起交给内核调度,对象头记录指向 ObjectMonitor,竞争线程进入等待队列。三者的取舍逻辑很清晰:能用用户态解决的就不进内核,能不进内核的就不占额外空间。

这些坑的正确绕法

最常见的坑是把 synchronized 加在实例方法上,又在静态方法上加锁,两种锁互不干扰造成伪同步。 实例方法锁的是 this,静态方法锁的是该类的 Class 对象,两者不是同一个监视器,因此 syncInstance() 与 static syncStatic() 可以同时进入。 表现是"明明加了 synchronized,数据还是错乱",而 code review 时很难一眼看出。防御办法是团队内统一锁对象约定:需要保护静态共享状态时,锁 XxxClass.class;需要保护实例状态时锁 this;两者并存时明确写出各自的保护范围。

其次是锁对象中途被替换,两个线程各锁各的对象,同步彻底失效。 比如锁的是 obj 字段,方法执行过程中给这个字段重新赋了值,那么另一个线程读到的就是新对象,两边锁的根本不是同一份数据。修法是把锁对象声明为 final,让引用不可变——这一条几乎零成本,却能消灭整类隐蔽 bug。

还有一个更隐蔽的坑:用 String 或 Integer 这类有缓存的常量对象当锁,两个无关的类可能锁到同一个对象上。 因为 "abc" 在常量池里只有一份,不同类里用同名字面量加锁会互相阻塞;Integer 缓存在 -128~127 区间,同一个数值也可能共享实例。正确做法是显式 new 一个私有对象当锁,或直接用 XxxClass.class。

synchronized 原理的实践经验是:前提先声明,失败路径给兜底,事故挡在上线前。 更具体的几条:临界区尽量小,不要在锁内做网络与磁盘操作;多把锁要固定获取顺序避免死锁;wait 调用前必须确认锁状态,循环写法用 while 而非 if。

代码里见真章

看一段能直接跑的代码,把上面的机制落到具体写法上:

// synchronized 与锁升级
private final Object lock = new Object();   // final 引用,防止锁对象被替换
private int counter;

public void add() {
   
    synchronized (lock) {
              // 字节码:monitorenter / monitorexit
        counter++;
    }
}

public static void sync() {
   }        // 静态方法锁的是 Class 对象,与实例锁互不干扰
// 升级路径:偏向(已废弃)→ 轻量级(CAS 自旋)→ 重量级(ObjectMonitor + 内核互斥)

这段代码值得盯三处:第一处,锁对象声明为 final,从根上杜绝"锁对象被换"的问题;第二处,锁的是一个私有 new Object() 而不是 this 或常量,避免了跨类干扰;第三处,静态方法与实例方法锁的是不同监视器这个容易被忽略的边界。面试讲到这一层,基本就稳了。

这题在面试里怎么问、怎么答

"请简单介绍一下 synchronized 原理,它在 Android 开发中起什么作用?"定位一句、场景一个、细节一处。synchronized 是 JVM 提供的互斥手段,靠对象头记录锁状态,兼具可见性与原子性保证。在 Android 里它常用于保护共享状态、保证只初始化一次(DCL 单例)、以及协调多线程对同一份数据的写入。

"synchronized 原理的底层原理是什么?能不能详细说一下?"期望的是动机、机制、代价三层,而不是一句"底层是 monitor"。按这个顺序讲:动机是互斥与内存可见;机制是 Mark Word 记录锁状态 + monitorenter/monitorexit 指令 + ObjectMonitor 管理等待队列;代价是线程竞争时会阻塞、上下文切换有成本、锁粒度粗导致并发度受限。 再补一句偏向锁在 JDK 15 废弃与默认关闭,这是版本演进层面的加分项。

"在使用 synchronized 原理时遇到过什么问题?"拿真实案例。 一个典型案例:某个单例用 if (instance == null) 判断加 synchronized 双重检查,代码 review 时被指出在别的类里也锁了 XxxClass.class 且临界区里还有别的操作,导致初始化偶尔被重入;定位到锁粒度过大;修复为把双检锁标准写法对齐并收窄临界区;验证是启动耗时下降且实例唯一性稳定。

"synchronized 原理和相关的替代方案相比,有什么优劣?"把对比维度列出来:性能、接入成本、生态、团队熟悉度。对比结论——synchronized 语义清晰、无需手动释放;ReentrantLock 支持超时、可中断、公平锁与多个条件队列,但必须 finally 释放;AtomicInteger 这类无锁类适合单变量原子更新。 面试里能按"临界区复杂度"分层选择(简单计数用原子类、复杂状态切换用 ReentrantLock)就说明真做过取舍。选型时把"锁对象被替换"这类代价摆到台面上,再决定是否引入。

再补一个工程上值得讲清的点:wait、notify、notifyAll 为什么必须在 synchronized 内调用。这三个方法作用的是条件本身——wait 把线程放进 ObjectMonitor 的等待集,notify 从等待集里挑一个唤醒。 Java 的设计是"检查条件"与"进入等待"必须原子完成,否则会出现"检查完条件还没睡,条件就被别人改掉了"的丢失唤醒问题;而且 wait 要释放锁才能让别人推进,不持锁调用会直接抛 IllegalMonitorStateException。 基于这个机制,标准写法是 while (!condition) { wait(); } 而不是 if——因为被唤醒不等于条件成立,可能存在虚假唤醒,也可能被别的线程抢先一步。用 while 重新检查,是把这条契约落实到代码里。

给正在准备面试的你

把锁升级画成一条阶梯:偏向(已废弃)→ 轻量级(栈上 Lock Record + CAS 自旋)→ 重量级(ObjectMonitor + 内核互斥),每级旁边标出触发条件(是否同一对象、预计等待时长)与代码上能观察到的现象(jstack 里的 BLOCKED 状态)。面试中关于 synchronized 的问题,关键在于能够从原理、应用、踩坑三个层面给出有深度的回答。

再补工程案例与踩坑——应用落点是把项目里锁 this、锁常量字符串、锁可重新赋值的字段这三类写法统一改掉,并用压测对比改前改后的并发正确性与吞吐。

复习时别孤立刷题:Thread 与 Runnable——BLOCKED 与 WAITING 两个状态的区别就出自锁与等待的分工。

划两句重点:锁信息存在对象头 Mark Word,竞争加剧时按偏向→轻量级→重量级单向升级;锁对象要 final 且避免常量,wait/notify 必须在 synchronized 内且用 while 检查条件。

下一篇聊 volatile:可见性、有序性与禁止重排——沿着今天这条主线继续往前走。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:Thread-与-Runnable:线程生命周期全解

下一篇预告:volatile:可见性、有序性与禁止重排

有任何问题欢迎在评论区留言交流。

相关文章
|
17天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8478 24
|
16天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
2817 14
|
15天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2021 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
14天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
10天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
4天前
|
人工智能 JSON Linux
【全网最详细】ComfyUI使用教程:下载+本地部署+配置+工作流搭建一篇搞定(2026最新版)
ComfyUI是一款免费开源的本地AI绘图工具,采用节点式工作流设计,支持文生图、图生图、局部重绘、放大、换脸等多种功能。可离线运行,依赖显卡加速,无需联网。支持自定义流程保存与分享,插件生态丰富,适合进阶用户。(239字)
|
10天前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)

热门文章

最新文章