ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

synchronized 底层深度解析

synchronized 底层深度解析
2.3.1 类锁和对象锁

面试官问这个,主要是考察你对锁作用域和内存模型的敏感度。

  • 对象锁(实例锁):

    • 用法:修饰普通实例方法,或者synchronized(this)。

    • 本质:锁的是当前对象的实例(Object)。

    • 场景:不同实例对象之间的锁互不干扰。

  • 类锁(全局锁):

    • 用法:修饰static静态方法,或者synchronized(MyClass.class)。

    • 本质:锁的是当前类的Class 对象(在 JVM 中全局唯一)。

    • 场景:锁住整个类,无论 new 了多少个实例,都共用这一把锁。

  • 面试连环问:“如果一个类里有一个synchronized静态方法和一个synchronized实例方法,两个线程能同时执行吗?”

    • 答:能。因为它们锁的根本不是同一个对象(一个是 Class 对象,一个是实例对象)。


2.3.2 synchronized 的优化(JVM 的极限微操)

早期synchronized是一把“重量级锁”,直接调用 OS 的互斥量(Mutex),涉及用户态和内核态的切换,极其耗时。JDK 1.6 引入了“锁膨胀”和“锁消除”等机制。

2.3.2.1 锁消除 (Lock Elimination)

  • 原理:JIT 编译器在编译时,通过逃逸分析(Escape Analysis)发现某段代码中的锁对象不可能被多个线程共享(即没有逃逸出当前线程)。

  • 面试案例:在方法内部new了一个StringBuffer并不断append。StringBuffer内部是用synchronized修饰的,但因为它只是局部变量,JIT 会直接把它的锁干掉,直接执行原生的非安全方法,极大提升性能。

2.3.2.2 锁膨胀/粗化 (Lock Coarsening)

  • 原理:如果 JVM 发现有一连串连续的、对同一个对象的“加锁-解锁”操作(比如在for循环里synchronized),频繁加解锁同样耗时。JVM 会将锁的范围扩大到整个循环外部。

  • 面试案例:

    java

    for (int i = 0; i < 100; i++) { synchronized(lock) { /* 做点小事 */ } } // JVM 优化后: synchronized(lock) { for (int i = 0; i < 100; i++) { /* 做点小事 */ } }

2.3.2.3 锁升级 (Lock Escalation) —— 必考核心
为了让锁的代价与竞争激烈程度相匹配,锁的状态只能升不能降:

  1. 无锁:对象刚创建。

  2. 偏向锁:只有一个线程访问。Mark Word 记录该线程 ID。后续该线程再次进入,比对 ID 即可,零开销(注意:由于维护成本高,JDK 15 后默认废弃)。

  3. 轻量级锁:出现多线程交替获取。利用CAS自旋获取锁,不进入内核态阻塞线程。适合执行极快的同步块。

  4. 重量级锁:竞争激烈,自旋超过一定次数(或线程数太多)。向操作系统申请互斥量(Mutex),没抢到锁的线程进入内核态阻塞,性能最差但不耗 CPU。


2.3.3 synchronized 的实现原理(看懂字节码与对象头)

2.3.3.1 MarkWord(对象头里的变色龙)
这是理解锁升级的物理载体。Java 对象头(Object Header)中的 Mark Word 在 64 位 JVM 下占 64 bit,它为了节省内存,复用了这块空间,根据锁的状态记录不同的信息:

  • 无锁:记录对象的 HashCode、分代年龄。锁标志位01。

  • 偏向锁:记录持有锁的线程 ID、Epoch。偏向标志位1,锁标志位01。

  • 轻量级锁:记录指向线程栈中 Lock Record 的指针。锁标志位00。

  • 重量级锁:记录指向底层ObjectMonitor对象的指针。锁标志位10。

(面试加分项:说明synchronized修饰代码块时,底层字节码使用monitorenter和monitorexit指令;修饰方法时,底层使用ACC_SYNCHRONIZED标志位。)

2.3.3.2 查看锁对象 MarkWord(实战工具)

  • 面试官可能会问:“你平时怎么证明锁升级了?”

  • 答:引入JOL (Java Object Layout)库。代码中调用ClassLayout.parseInstance(obj).toPrintable()。通过打印出来的二进制,可以直观地看到锁标志位从01变00再变10的全过程。


2.3.4 查看 ObjectMonitor(重量级锁的真面目)

当锁升级到重量级锁时,Mark Word 里存的指针指向的就是 C++ 底层的一个复杂结构体:ObjectMonitor(位于 HotSpot 源码objectMonitor.hpp)。

它的核心内部结构(面试高频):

  • _owner:指向当前持有锁的线程。

  • _cxq:竞争队列(Contention Queue),单链表。新来的、抢锁失败的线程会被放入这里。

  • _EntryList:候选队列,存放从_cxq转移过来、准备竞争锁的线程。

  • _WaitSet:等待集合,存放调用了Object.wait()方法被挂起的线程。

底层执行流程:

  1. 线程执行monitorenter,尝试把_owner设置为自身。

  2. 如果_owner为空,获取锁成功。

  3. 如果不为空,该线程会被封装成ObjectWaiter节点,放入_cxq队列。

  4. 底层通过调用操作系统的park()方法(本质是 Linux 的futex系统调用),将该线程挂起(进入内核态阻塞)。

  5. 持有锁的线程执行完毕后,释放锁,并唤醒_cxq或_EntryList里的线程去抢锁。


💡 面试闭环:将知识点串联

如果面试官问:“聊聊synchronized的优缺点?”

高级回答:
“早期的synchronized因为直接调用操作系统的互斥量,导致频繁的用户态与内核态切换,性能很差。但自从 JDK 1.6 引入了锁升级机制后,它变得更加智能化:只有一个线程时用偏向锁(无开销),交替执行时用轻量级锁(CAS 自旋,用户态),只有竞争极其激烈时才升级为重量级锁(调用 ObjectMonitor,进入内核态阻塞)。

再加上 JIT 编译器的锁消除和锁粗化优化,现在的synchronized性能已经非常优异,在大多数简单的互斥场景下,甚至比ReentrantLock更好,因为它是 JVM 原生支持的,不需要手动释放锁,极大降低了死锁风险。”

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进