Java中的偏向锁、轻量级锁与重量级锁详解
字数 1708 2025-11-14 16:41:13
Java中的偏向锁、轻量级锁与重量级锁详解
描述
Java中的synchronized关键字在JDK 1.6之后引入了锁升级机制,包括偏向锁、轻量级锁和重量级锁三种状态。这种设计是为了在保证线程安全的同时,减少锁操作带来的性能开销。锁会根据竞争情况从低到高逐步升级,优化多线程环境下的同步性能。
锁升级的背景与目的
- 早期synchronized直接使用操作系统互斥锁(重量级锁),性能较差
- 统计发现大部分锁在运行时不存在竞争或竞争程度很低
- 锁升级机制通过延迟使用重量级锁来优化性能
对象头与锁状态
- 每个Java对象在内存中分为三部分:对象头、实例数据、对齐填充
- 对象头包含Mark Word(存储对象运行时数据)和类型指针
- Mark Word在不同锁状态下会存储不同的内容:
- 无锁状态:存储对象的hashCode、分代年龄等
- 偏向锁:存储偏向线程ID、偏向时间戳等
- 轻量级锁:指向栈中锁记录的指针
- 重量级锁:指向互斥量(monitor)的指针
偏向锁(Biased Locking)
- 设计目标:消除无竞争情况下的同步开销
- 工作原理:
- 第一个获取锁的线程会将线程ID写入对象头
- 之后该线程再次获取锁时,只需检查线程ID是否匹配
- 如果匹配,直接进入同步代码块,无需原子操作
- 偏向锁的获取过程:
- 检查对象头中是否存储了当前线程ID
- 如果已存储,直接获得锁
- 如果未存储,通过CAS操作尝试将线程ID写入对象头
- 偏向锁的撤销:
- 当其他线程尝试获取锁时,需要撤销偏向锁
- 暂停拥有偏向锁的线程
- 检查线程是否存活,如果已结束则直接允许新线程获取锁
- 如果仍存活,升级为轻量级锁
轻量级锁(Lightweight Locking)
- 适用场景:线程交替执行同步块,不存在真正竞争
- 加锁过程:
- 在当前线程的栈帧中创建锁记录(Lock Record)
- 将对象头的Mark Word复制到锁记录中(Displaced Mark Word)
- 使用CAS操作将对象头替换为指向锁记录的指针
- 如果成功,获得锁;如果失败,表示存在竞争
- 解锁过程:
- 使用CAS操作将Displaced Mark Word替换回对象头
- 如果成功,解锁完成;如果失败,表示存在竞争,需要膨胀为重量级锁
- 自旋优化:
- 在轻量级锁竞争失败后,线程不会立即阻塞
- 而是进行有限次数的自旋(循环检查锁状态)
- 如果自旋期间锁被释放,可以避免线程切换的开销
重量级锁(Heavyweight Locking)
- 适用场景:存在真正的线程竞争
- 实现机制:
- 基于操作系统的互斥量(mutex)实现
- 涉及线程的阻塞和唤醒,需要从用户态切换到内核态
- 监视器(Monitor)结构:
- 每个Java对象都与一个Monitor关联
- Monitor包含多个重要组件:
- _owner:指向持有锁的线程
- _EntryList:等待锁的线程队列
- _WaitSet:调用wait()方法的线程队列
- 重量级锁的工作流程:
- 线程尝试获取锁时,如果锁已被占用,进入EntryList等待
- 当锁释放时,唤醒EntryList中的线程竞争锁
- 被唤醒的线程需要重新竞争锁
锁升级的完整流程
- 对象初始状态为无锁状态
- 当第一个线程访问时,升级为偏向锁
- 当第二个线程尝试获取锁时,撤销偏向锁,升级为轻量级锁
- 如果轻量级锁竞争激烈(自旋失败),升级为重量级锁
- 重量级锁不会降级,除非进行垃圾回收
性能对比与优化建议
- 偏向锁:适用于单线程重复加锁的场景
- 轻量级锁:适用于线程交替执行的场景
- 重量级锁:适用于高并发竞争的场景
- 优化建议:
- 如果确定存在竞争,可以通过JVM参数禁用偏向锁(-XX:-UseBiasedLocking)
- 合理控制同步块的大小,减少锁的持有时间
- 考虑使用更细粒度的锁或并发容器
实际应用中的注意事项
- 锁升级是JVM自动完成的,对开发者透明
- 了解锁升级机制有助于编写更高效的并发代码
- 在高度竞争的场景下,可以考虑使用ReentrantLock等更灵活的锁机制
这种锁升级机制体现了Java在并发性能优化上的精细设计,通过在适当的时候使用适当的锁级别,实现了性能与功能的平衡。