一、大白话版

1. synchronized 是干嘛的?

synchronized 是 Java 里解决“多线程抢同一把锁”问题的关键字。你可以把它想象成厕所的门锁

  • 一个线程进了厕所,就把门锁上(获得锁)。
  • 其他线程想进,只能在门外等着(阻塞)。
  • 里面的线程用完,打开门(释放锁),外面的人再抢。

2. JDK 8 里 synchronized 怎么实现“锁”?

JDK 8 不是一上来就用很重的操作系统锁,而是会根据竞争激烈程度自动升级锁,就像这样:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  • 无锁:初始状态,没任何人抢。
  • 偏向锁:只有一个线程反复进厕所,门锁记住这个人的“名字”,以后他再来直接推门进,不检查。省事。
  • 轻量级锁:来了另一个线程也想进,俩人轻度竞争。此时不惊动操作系统,用 CAS(自旋) 原地转圈等一会儿,如果很快能拿到锁,就省去操作系统阻塞的开销。
  • 重量级锁:竞争激烈,自旋等太久了太耗CPU,于是交给操作系统管(mutex),线程被真正挂起,等着被唤醒。

3. 厕所门上的“小纸条”——对象头

每个 Java 对象头上有一小块内存,叫 Mark Word,就像门上贴的小纸条,记录锁的状态、线程ID、哈希码等。

4. 大白话总结

  • 最初没人抢 → 无锁。
  • 始终一个人进 → 偏向锁(记住你)。
  • 偶尔两个人抢 → 轻量级锁(原地等一会儿,不行就升级)。
  • 很多人抢 → 重量级锁(叫操作系统来排队)。

这样设计是为了性能:绝大多数锁竞争并不激烈,用轻量级手段就够了。


二、专业版(含对象头结构详解 & 锁升级原理)

1. 对象头结构(以 HotSpot JVM,64位为例)

Java 对象的内存布局分为三部分:

  • 对象头 (Header)
    • Mark Word(8字节)
    • Klass Pointer(类型指针,默认开启指针压缩时为4字节)
  • 实例数据 (Instance Data)
  • 对齐填充 (Padding)

其中 Mark Word 是锁机制的核心。64位 JVM 下 Mark Word 各状态布局如下(图1):

无锁状态(001):
|--------------------------------------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | 001 |
|--------------------------------------------------|
  63                          0  (bit 位置简化)

偏向锁状态(101):
|-----------------------------------------------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | 偏向锁标记(1) | 锁标记(01) |
|-----------------------------------------------------------|
  63                                                        0

轻量级锁状态(00):
|--------------------------------------------------|
|           指向锁记录 (Lock Record) 的指针          | 00 |
|--------------------------------------------------|
  63                                              0

重量级锁状态(10):
|--------------------------------------------------|
|           指向互斥量 (monitor) 的指针            | 10 |
|--------------------------------------------------|
  63                                            0

GC标记(11):
|--------------------------------------------------|
|                 空(或其他GC信息)                | 11 |
|--------------------------------------------------|

注意:实际 bit 位分配因 JVM 版本和平台有细微差别,但核心逻辑不变。低位 2~3 位是锁标志位。

锁标志位对照表

锁状态 标记位 (bits)
无锁 001
偏向锁 101
轻量级 00
重量级 10
GC标记 11

2. 锁升级详细流程

(1) 偏向锁 (Biased Locking)
  • 目的:消除无竞争下的同步操作。
  • 原理:当锁被第一个线程获取时,JVM 将 Mark Word 设为偏向模式,并记录线程 ID。以后该线程再进入同步块,只用检查 Mark Word 中的线程 ID 是否是自己(仅一次 CAS),不是则尝试重偏向或升级。
  • JDK8 默认行为:偏向锁默认开启,但有 延迟(约 4 秒),防止 JVM 启动初期大量锁争用导致频繁偏向撤销。
  • 撤销:当另一个线程尝试竞争偏向锁时,需要在全局安全点(Safepoint)撤销偏向锁,可能升级为轻量级锁。
(2) 轻量级锁 (Lightweight Locking)
  • 触发:偏向锁失效或有另一个线程来竞争。
  • 机制
    1. 线程在自己的栈帧中创建 锁记录 (Lock Record),尝试用 CAS 将对象头的 Mark Word 替换为指向锁记录的指针。
    2. 若成功,线程获得轻量级锁,Mark Word 变为 00
    3. 若 CAS 失败,说明有竞争。线程会 自旋(循环尝试几次,JDK 8 中自旋次数可通过 -XX:PreBlockSpin 调整,自适应自旋也会开启)。
    4. 自旋一定次数后仍未成功,锁膨胀为重量级锁,Mark Word 变为 10,指针指向操作系统的 monitor 对象。
(3) 重量级锁 (Heavyweight Locking)
  • 实现:基于操作系统的 mutex 互斥量。
  • 行为:未获得锁的线程被阻塞(从用户态陷入内核态),进入等待队列。
  • 开销:上下文切换成本高,但避免了 CPU 空转。
(4) 锁升级不可逆(除偏向锁可重偏向)
  • 一旦升级为重量级锁,不会降级。这是设计决策,保证高竞争下的稳定。

3. 关键优化(JDK 8)

  • 锁消除 (Lock Elimination):JIT 编译器分析出某个锁对象只被单线程访问时,直接去掉同步(依赖逃逸分析)。
  • 锁粗化 (Lock Coarsening):JIT 将相邻的多个同步块合并为一个同步块,减少加锁/解锁次数。
  • 自适应自旋 (Adaptive Spinning):自旋时间不再固定,根据前一次同一锁的自旋结果动态调整。

4. 源码层面的关键入口

  • ObjectMonitor::enter (重量级锁)
  • synchronizer.cppbiasedLocking.cpp
  • 字节码指令 monitorenter / monitorexit

5. 图文总结:锁升级状态迁移图

       初始(无锁001)
            │
            │ 第一个线程进入
            ▼
       偏向锁(101)- thread ID
            │
            │ 另一线程竞争
            ▼
       轻量级锁(00) + 自旋
            │
            │ 自旋超时 or 竞争加剧
            ▼
       重量级锁(10) → mutex 阻塞

6. 重要参数(JDK 8)

JVM 参数 作用
-XX:+UseBiasedLocking 开启偏向锁(默认 true)
-XX:BiasedLockingStartupDelay 偏向锁延迟时间(默认 4000 ms)
-XX:+UseHeavyMonitors 禁用锁升级(纯重量锁,调试用)
-XX:PreBlockSpin 轻量锁自旋次数(JDK 8 已自适应)
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐