详细说说synchronized锁升级的过程

Java synchronized 锁升级过程详解

程序员面试必备资料

Java中的synchronized锁从JDK 1.6开始实现了锁升级机制,这是JVM为了在不同竞争场景下都能获得最优性能而设计的精巧优化。锁升级过程遵循"偏向锁→轻量级锁→重量级锁"的路径,下面我将详细解析每个阶段的转换过程。

一、对象头结构基础

在了解锁升级前,需要先理解Java对象头的结构(以64位JVM为例):

|---------------------------------------------------------------------|
|                      Mark Word (64 bits)                            |
|---------------------------------------------------------------------|
| 锁状态       | 存储内容                                              |
|---------------------------------------------------------------------|
| 无锁         | unused:25 | identity_hashcode:31 | unused:1 | age:4 | 0|01 |
| 偏向锁       | thread:54 | epoch:2 | unused:1 | age:4 | 1 | 01      |
| 轻量级锁     | 指向栈中锁记录的指针:62 | 00                          |
| 重量级锁     | 指向互斥量(mutex)的指针:62 | 10                        |
| GC标记       | 空(不需要记录信息) | 11                                |

二、锁升级完整流程

1. 初始状态 - 无锁 (001)

当对象刚被创建时:

  • 锁标志位是001
  • 没有线程持有该对象锁
  • 可以计算identity hashcode(一旦计算会禁用偏向锁)

2. 偏向锁阶段 (101)

触发条件

  • 第一个线程访问同步块
  • JVM启用了偏向锁(默认开启,但有4秒延迟)

升级过程

  1. 检查对象头中的锁标志位是否为001(可偏向状态)
  2. 通过CAS操作尝试将Mark Word中的Thread ID替换为当前线程ID:
    • 成功:进入偏向模式,锁标志位变为101
    • 失败:说明存在竞争,开始撤销偏向锁

偏向锁数据结构

|---------------------------------------------------------------------|
|  thread:54 | epoch:2 | unused:1 | age:4 | 1 | 01                   |
|---------------------------------------------------------------------|

特点

  • 后续该线程进入同步块只需检查Thread ID是否匹配
  • 几乎不产生同步开销
  • 适合单线程重复访问的场景

撤销偏向锁
当其他线程尝试获取已被偏向的锁时:

  1. 暂停持有偏向锁的线程(到达安全点)
  2. 检查原持有线程是否仍处于同步块中:
    • 已退出:恢复到无锁状态(001
    • 仍活跃:升级为轻量级锁(00

3. 轻量级锁阶段 (00)

触发条件

  • 多个线程交替访问同步块(无真正竞争)
  • 偏向锁被撤销

升级过程

  1. 在当前线程栈帧中创建锁记录(Lock Record)
  2. 将对象头Mark Word复制到锁记录中(Displaced Mark Word)
  3. 通过CAS尝试将对象头替换为指向锁记录的指针:
    • 成功:获取轻量级锁
    • 失败:存在竞争,开始自旋或升级

轻量级锁数据结构

对象头:
|---------------------------------------------------------------------|
| 指向栈中锁记录的指针:62 | 00                                       |
|---------------------------------------------------------------------|

栈帧中的锁记录:
|---------------------------------------------------------------------|
| Displaced Mark Word | 对象指针                                     |
|---------------------------------------------------------------------|

自旋优化

  • 失败线程不会立即阻塞,而是自旋尝试获取锁
  • 自旋次数由JVM自适应调整(基于上次获取情况)
  • 避免线程切换的开销

升级到重量级锁
当:

  • 自旋超过阈值(默认10次)
  • 或等待线程数超过CPU核数的一半

4. 重量级锁阶段 (10)

实现机制

  • 对象头指向操作系统级别的互斥量(mutex)
  • 未获取锁的线程进入阻塞状态
  • 依赖操作系统的线程调度

重量级锁数据结构

|---------------------------------------------------------------------|
| 指向互斥量(mutex)的指针:62 | 10                                    |
|---------------------------------------------------------------------|

特点

  • 上下文切换开销大
  • 适合高竞争场景
  • 线程阻塞会触发操作系统调度

三、锁升级状态转换图

第一个线程访问
第二个线程访问
自旋失败/多线程竞争
原线程退出同步块
锁释放
锁释放
无锁(001)
偏向锁(101)
轻量级锁(00)
重量级锁(10)

四、关键优化细节

1. 批量重偏向(Bulk Rebias)

  • 问题:一类对象被多个线程交替访问(如生产者-消费者模式)
  • 解决:当一类对象的偏向锁撤销超过阈值(默认20次)
  • 操作:JVM会批量重偏向这些对象到新线程

2. 批量撤销(Bulk Revoke)

  • 问题:一类对象明显存在多线程竞争
  • 解决:当一类对象的偏向锁撤销超过另一阈值(默认40次)
  • 操作:JVM会禁用该类的偏向锁功能

3. 偏向锁延迟启用

  • JVM启动后4秒(可通过-XX:BiasedLockingStartupDelay=0禁用)
  • 避免启动阶段大量类加载导致的偏向锁撤销

4. 锁降级

  • 重量级锁不会降级为轻量级锁
  • 轻量级锁释放后直接回到无锁状态

五、性能影响对比

场景偏向锁轻量级锁重量级锁
单线程访问纳秒级需要CAS系统调用
低竞争交替访问需撤销微秒级系统调用
高竞争频繁撤销自旋消耗CPU线程阻塞
内存占用无额外开销栈帧锁记录重量级元数据

六、最佳实践

  1. 减少同步范围:只在必要代码块加锁
  2. 降低锁粒度:拆分大锁为多个小锁
  3. 避免长时间同步:减少持有锁的时间
  4. 监控锁竞争:使用JConsole或VisualVM观察锁状态
  5. 高竞争场景:考虑使用java.util.concurrent中的显式锁

锁升级机制使得synchronized在无竞争和低竞争场景下性能接近无锁,同时在高竞争时仍能保证正确性,是Java并发性能优化的重要成果。

程序员面试必备资料

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值