JVM的锁优化策略
JVM的锁优化策略是一系列为提高并发性能而设计的技术,目的是减少锁操作的开销。主要包括:
- 锁消除:编译器在运行时检测到不可能存在共享数据竞争的锁,将其消除
- 锁粗化:将连续多次对同一对象的加锁、解锁操作合并为一次范围更大的锁操作
- 自适应自旋:根据以往自旋等待的成功率,动态调整自旋时间
- 偏向锁/轻量级锁/重量级锁:根据竞争情况动态调整的锁状态升级路径
为什么要弄这个优化
早期的synchronized是重量级锁,直接依赖操作系统的互斥锁(mutex lock),每次加锁/解锁都需要从用户态切换到内核态,性能开销巨大。
优化的目标:
-
减少不必要的同步开销:大多数对象在其生命周期中不会被多个线程竞争
-
提高无竞争或低竞争场景的性能:在单线程或低并发下接近无锁性能
-
渐进式开销:只在真正需要时才使用重量级锁
这些优化完全由JVM自动完成,程序员只需要正常使用synchronized关键字
详细锁升级路径解析
阶段1:无锁状态
- 对象头Mark Word结构:包含hashCode、分代年龄、锁标志位(01)
- 适用场景:对象刚创建,还未被任何线程锁定
阶段2:偏向锁
偏向锁是 Java 虚拟机 (JVM) 针对synchronized关键字实现的一种锁优化机制,其设计目标是为了在 “无实际竞争” 或 “长时间仅被一个线程访问” 的同步代码场景下,最大限度地减少同步带来的性能开销。
简单来说,它“偏向”于第一个获取它的线程。在接下来的执行中,如果没有其他线程来竞争,这个线程后续再进入同步代码块时,无需执行任何与锁相关的操作(如CAS、操作系统互斥等),直接执行即可,就像没有锁一样。
工作原理:
当锁对象第一次被线程获取时,JVM会将对象头中的标记设置为“偏向模式”,并记录获取锁的线程ID。之后,这个线程再进入该锁相关的同步块时,只需检查对象头中的线程ID是否指向自己:
- 如果是,则直接通行,开销极低。
- 如果不是,说明发生了竞争,偏向锁需要被撤销,并可能升级为更高级的锁(轻量级锁或重量级锁)。
// Mark Word结构(偏向锁状态)
// 线程ID(54位) | Epoch(2位) | 分代年龄(4位) | 1(偏向模式) | 01(锁标志)
32位的JVM中hashCode()调用会禁用偏向锁(因为要存储hashCode)
阶段3:轻量级锁(自旋锁)
轻量级锁 是JVM在多线程轻度竞争场景下,为了优化同步性能而设计的一种锁机制。它是锁升级路径(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁)中的关键一环。
在多数情况下,同步代码块在整个运行期间并不存在真正的多线程竞争,而是由不同线程交替执行。为了避免直接调用操作系统的重量级互斥锁(开销大),JVM尝试使用一种更“轻量”的方式——在当前线程的栈帧中创建一个名为“锁记录”的空间,并使用CPU的CAS原子指令尝试将对象头中的Mark Word更新为指向该锁记录的指针。如果更新成功,则当前线程获得该轻量级锁。
工作原理:
-
当线程进入同步代码块时,如果同步对象处于可偏向状态或无锁状态,JVM会先在当前线程的栈帧中创建一个锁记录空间,用于存储对象当前的Mark Word拷贝(称为Displaced Mark Word)。
-
JVM使用CAS操作,尝试将对象头中的Mark Word替换为指向该锁记录的指针,并将锁记录中的
owner指针指向该对象。 -
如果CAS成功,当前线程获得锁,对象头锁标志位变为
00,表示轻量级锁状态。 -
如果CAS失败(通常因为其他线程已持有该轻量级锁),当前线程会通过自旋的方式尝试重新获取锁。若自旋一定次数后仍未成功,则说明竞争加剧,锁将膨胀为重量级锁。
阶段4:重量级锁
重量级锁是JVM内置锁(synchronized)在锁升级过程中的最终状态,当多个线程激烈竞争同一个锁时,JVM会将轻量级锁或偏向锁升级为重量级锁。
核心特点:
- 基于操作系统的互斥量(Mutex) 实现
- 线程竞争失败后会被挂起(Blocked) ,进入内核态等待
- 涉及用户态到内核态的切换,开销较大
- 通过操作系统的线程调度机制实现同步
当轻量级锁竞争失败,大量线程自旋导致CPU空转,重量级锁将竞争失败的线程挂起,释放CPU给其他线程使用。每个Java对象都与一个Monitor关联,重量级锁时启用。
触发条件:
- 自旋超过阈值(默认10次)
- 等待线程数>CPU核数/2