JVM的锁优化策略:从偏向锁到锁消除
在高并发环境下,锁的使用是保证线程安全的重要手段,但频繁的加锁解锁也会带来显著的性能开销。为了减少同步操作带来的损耗,Java虚拟机(JVM)实现了一系列精妙的锁优化技术,旨在不牺牲线程安全的前提下提升程序执行效率。
偏向锁 (Biased Locking)
目标:消除无竞争情况下的同步开销。
原理:当锁对象第一次被线程获取后,JVM会将其标记为“偏向模式”,并将线程ID记录在对象头中。之后该线程再次请求该锁时,无需进行任何实际的同步操作(如CAS),可以直接进入临界区。
适用场景:适用于锁被单个线程长时间独占的场景。在竞争激烈的环境下,偏向锁的撤销成本可能高于其收益。轻量级锁 (Lightweight Locking)
目标:在低竞争情况下,避免线程在操作系统层面被挂起(用户态到内核态的切换)。
原理:当偏向锁失败(即存在另一个线程尝试获取锁)时,JVM不会立即升级为重量级锁。它会将对象头中的标记替换为一个指向当前线程栈中锁记录的指针(即“锁记录”空间)。获取锁的过程通过CAS操作完成。若成功,线程继续执行;若失败,说明存在竞争,锁将“膨胀”为重量级锁。
适用场景:线程交替执行,持有锁时间很短的场景。自旋锁与自适应自旋 (Spin Lock & Adaptive Spinning)
目标:减少线程阻塞和唤醒的开销。
原理:当轻量级锁膨胀为重量级锁后,请求锁失败的线程不会立即被挂起(进入阻塞状态)。相反,它会执行一个忙循环(自旋),不断尝试获取锁。其假设是:锁被持有的时间通常很短,自旋等待比挂起再唤醒的代价更小。
自适应自旋:JVM会根据历史数据动态调整自旋次数。如果某个锁对象最近经常成功通过自旋获得,JVM会允许更长的自旋时间;反之,则减少或直接跳过自旋。锁消除 (Lock Elimination)
目标:移除不可能存在共享资源竞争的、不必要的锁操作。
原理:JVM的即时编译器(JIT)在运行时进行逃逸分析。如果一个对象被证明不会“逃逸”出当前线程(即其他线程无法访问到它),那么该对象上的所有同步操作都是无效的,可以被安全地消除。
示例:
java
public String concatenate() {
// StringBuffer是线程安全的,内部方法有synchronized修饰
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
return sb.toString();
}
在此方法中,StringBuffer 对象 sb 是局部变量,其引用不会发布到其他线程。因此,JIT编译器可以判定所有对 sb 的同步操作(如 append
内的锁)都是多余的,从而将其消除,优化后的代码性能类似于使用 StringBuilder。
启用:锁消除是JVM自动进行的优化,通常在Server模式下(使用 -server 参数)会更激进。可以通过 -XX:+DoEscapeAnalysis
开启逃逸分析,-XX:+EliminateLocks 开启锁消除。
