synchronized 是怎么实现的?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

这道题我愿意称之为 Java 并发面试的 "试金石"。能回答 "字节码指令 monitorenter/monitorexit" 的算及格,能说出 Monitor、对象头、Mark Word 的算良好,能把锁升级过程(偏向锁 → 轻量级锁 → 重量级锁)讲清楚的,面试官直接在心里给你打优秀。

面试考察点

  1. 字节码层面:面试官想知道你是否了解 synchronized 在字节码层面是怎么体现的——同步代码块用的是 monitorenter/monitorexit 指令,同步方法用的是 ACC_SYNCHRONIZED 标志位。这是最基本的。

  2. JVM 层面:是否理解 Monitor(管程/监视器)的概念,以及对象头中 Mark Word 和锁状态的关系。这部分是真正考察 JVM 功底的地方。

  3. 锁升级机制:JDK 6 引入的锁升级是 synchronized 优化的核心,也是面试的高频追问点。偏向锁、轻量级锁、重量级锁分别解决什么问题?升级过程是怎样的?触发条件是什么?

核心答案

synchronized 的实现可以分三个层面来理解:

层面 实现方式 关键机制
Java 语法层 关键字修饰方法或代码块 编译后转成对应的字节码指令或标志位
字节码层 monitorenter/monitorexit 指令或 ACC_SYNCHRONIZED 标志 JVM 根据 Monitor 实现加锁/解锁
JVM 层 基于 Monitor(ObjectMonitor)+ 对象头 Mark Word 锁信息记录在对象头的 Mark Word 中
优化层(JDK 6+) 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁 根据竞争程度逐步升级,降低开销

深度解析

一、字节码层面——两种实现方式

synchronized 用在不同位置,编译后的字节码是不一样的:

1. 同步代码块:monitorenter / monitorexit

public void syncBlock() {
    synchronized (this) {
        // 业务逻辑
    }
}

编译后字节码(简化):

monitorenter       // 获取 Monitor 锁
// 业务逻辑对应的字节码
monitorexit        // 释放 Monitor 锁
goto 正常退出
Exception handler:
monitorexit        // 异常路径也要释放锁!第二个 monitorexit 就是兜底

注意看,编译器生成了 两个 monitorexit。第一个是正常路径,第二个在异常处理表中,保证即使同步块内抛异常也能释放锁。这比 ReentrantLock 必须手动在 finallyunlock() 要安全得多——JVM 帮你兜底了。

2. 同步方法:ACC_SYNCHRONIZED 标志位

public synchronized void syncMethod() {
    // 业务逻辑
}

编译后字节码中方法表多了 ACC_SYNCHRONIZED 标志(0x0020),没有显式的 monitorenter/monitorexit。JVM 在调用方法时检查这个标志位,如果设置了就自动加锁,方法返回时自动释放。

二、Monitor——JVM 层的核心

monitorenter 指令到底干了什么?答案是去找对象的 Monitor。

每个 Java 对象天生就关联着一个 Monitor。在 JVM 内部,这个 Monitor 的实现类是 ObjectMonitor(C++ 实现),核心数据结构如下:

上图展示了 ObjectMonitor 的核心结构和线程状态流转,关键点在于:

  • _owner:指向当前持有锁的线程。monitorenter 成功后,线程把自己设为 _owner
  • _count:锁的计数器,用来实现可重入。每次重入 +1,退出 -1,减到 0 才真正释放锁
  • _EntryList:当锁被占着,新来的线程会进入这个队列阻塞等待(对应 BLOCKED 状态)
  • _WaitSet:持有锁的线程调用 Object.wait() 后,会释放锁并进入这个队列等待(对应 WAITING 状态)。被 notify() 唤醒后,会从 _WaitSet 移到 _EntryList,重新竞争锁

这就是为什么 synchronized 是 "重量级锁" 的原因——ObjectMonitor 底层依赖操作系统的 Mutex Lock(互斥锁),线程的阻塞和唤醒需要从用户态切换到内核态,开销很大。

三、对象头——锁信息的存储位置

锁信息存在哪里?存在对象头(Object Header)的 Mark Word 中。

Java 对象在 JVM 中的内存布局分三部分:对象头(Header)+ 实例数据(Instance Data)+ 对齐填充(Padding)。其中对象头中的 Mark Word 是关键,它记录了锁状态、GC 年龄、哈希码等信息。

上图展示了 Mark Word 在不同锁状态下的存储内容和标志位。可以看到,锁升级的过程本质上是 Mark Word 中存储的信息不断变化的过程——从最初的哈希码和 GC 年龄,变成线程 ID(偏向锁),再变成指向 Lock Record 的指针(轻量级锁),最后变成指向 ObjectMonitor 的指针(重量级锁)。

四、锁升级过程——JDK 6 的核心优化

JDK 6 之前,synchronized 一上来就是重量级锁(直接走 ObjectMonitor + OS Mutex),代价很大。JDK 6 引入了锁升级机制,根据竞争程度逐步升级,绝大多数场景根本用不到重量级锁。

上图展示了 synchronized 锁的完整升级路径,每一级的设计目的和适用场景都不同:

  • 偏向锁:核心思想是 "如果一个锁一直只被同一个线程访问,那就连 CAS 都不做"。第一次获取锁时,把线程 ID 写入 Mark Word,之后同一线程再进来,只需比对线程 ID 是否一致,一致就直接进入同步块。零开销。但如果出现了第二个线程来竞争,偏向锁就要被撤销(需要等到全局安全点 STW 才能撤销,开销不小)。

  • 轻量级锁:偏向锁被撤销后升级为轻量级锁。JVM 在当前线程的栈帧中创建一个 Lock Record,然后用 CAS 尝试把对象头 Mark Word 替换为指向 Lock Record 的指针。成功的线程获取锁,失败的线程 自旋(循环重试)而不是直接阻塞。自旋的好处是避免了内核态切换的开销,但如果锁被持有时间长,自旋就是在浪费 CPU。

  • 重量级锁:自旋超过一定次数(自适应自旋),或者竞争线程太多,就升级为重量级锁。此时 Mark Word 存储指向 ObjectMonitor 的指针,竞争失败的线程进入 _EntryList 阻塞等待(涉及 OS Mutex,用户态 → 内核态切换)。

五、自适应自旋——JDK 6 的另一个优化

"自旋" 是轻量级锁阶段竞争失败时的策略。但自旋次数设多少合适?太多了浪费 CPU,太少了又容易升级为重量级锁。

JDK 6 引入了 自适应自旋(Adaptive Spinning):JVM 根据上一次在同一个锁上的自旋情况来动态决定本次自旋次数。如果上一次自旋成功获取到了锁,JVM 会认为这次也大概率能成功,允许多自旋几次;如果上一次自旋失败了,那这次可能直接跳过自旋,直接升级为重量级锁。

面试高频追问

  1. 追问一:偏向锁在 JDK 15 之后被废弃了,为什么?

    偏向锁的撤销需要等到全局安全点(STW),这个开销在高度竞争的场景下反而成了负担。现代 JVM 的优化已经让轻量级锁足够高效,偏向锁的收益越来越小,维护成本却很高。JDK 15 默认关闭了偏向锁(-XX:-UseBiasedLocking),JDK 18 正式移除。

  2. 追问二:synchronized 和 ReentrantLock 底层实现有什么不同?

    synchronized 基于 JVM 内置的 ObjectMonitor,依赖对象头的 Mark Word 和操作系统 Mutex;ReentrantLock 基于 AQS(AbstractQueuedSynchronizer),依赖 volatile int state + CAS + CLH 队列,全程在用户态完成,不需要 OS 介入。

  3. 追问三:什么是锁消除和锁粗化?

    锁消除:JIT 编译器通过逃逸分析发现某个对象只在一个线程内使用,不可能被其他线程访问,就自动消除 synchronized(比如 StringBufferappend() 在局部变量场景下)。锁粗化:JIT 编译器发现对同一个对象反复加锁解锁(比如循环内加锁),就把锁范围扩大到整个循环外部,减少加锁解锁次数。

常见面试变体

  • "synchronized 的锁升级过程是怎样的?"
  • "偏向锁、轻量级锁、重量级锁分别是什么?"
  • "synchronized 和 Lock 的底层实现有什么区别?"
  • "JDK 6 对 synchronized 做了哪些优化?"

记忆口诀

锁升级四步走:无锁 → 偏向锁(记线程 ID)→ 轻量级锁(CAS + 自旋)→ 重量级锁(OS Mutex 阻塞)。单向升级不回头,竞争越激烈锁越重。配合 自适应自旋锁消除锁粗化 三大优化,JDK 6 之后 synchronized 再也不是 "慢" 的代名词。

总结

synchronized 的实现从上到下分四层:Java 语法层(关键字)→ 字节码层(monitorenter/monitorexit)→ JVM 层(ObjectMonitor + Mark Word)→ 优化层(锁升级)。面试中先说字节码层面的两种实现,再讲 Monitor 的核心数据结构(_owner_EntryList_WaitSet),最后展开锁升级过程,这条线下来面试官基本满意。别忘了提一句 JDK 6 的三大优化(自适应自旋、锁消除、锁粗化),这是加分项。