一、为什么需要锁优化
多线程编程中,锁是保证线程安全的重要手段,但锁的使用往往会带来性能开销。比如,当多个线程竞争同一个锁时,会导致线程阻塞,进而影响程序的整体吞吐量。JVM 为了减少这种开销,引入了偏向锁和轻量级锁的概念,它们的目标是在低竞争环境下减少锁带来的性能损耗。
举个例子,假设我们有一个简单的计数器类:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
这里的 increment() 方法使用了 synchronized 关键字来保证线程安全。如果多个线程频繁调用这个方法,在高并发场景下,传统的重量级锁会导致大量线程阻塞,降低性能。而偏向锁和轻量级锁可以在某些情况下避免这种问题。
二、偏向锁:减少无竞争时的开销
偏向锁的核心思想是:如果一个锁一直被同一个线程访问,那么 JVM 可以优化掉这个锁的大部分开销。具体来说,偏向锁会在对象头中记录当前持有锁的线程 ID,后续该线程再次获取锁时,无需进行任何同步操作。
示例:偏向锁的工作机制
public class BiasedLockExample {
public static void main(String[] args) {
Object lock = new Object();
// 第一次获取锁,JVM 会启用偏向锁
synchronized (lock) {
System.out.println("第一次获取锁,偏向当前线程");
}
// 同一个线程再次获取锁,无需同步
synchronized (lock) {
System.out.println("同一个线程再次获取锁,偏向锁生效");
}
}
}
注释说明:
- 第一次进入同步块时,JVM 会检测到当前没有竞争,于是启用偏向锁。
- 后续同一个线程再次进入同步块时,JVM 发现锁已经偏向当前线程,直接放行,无需额外操作。
适用场景
- 适用于单线程或极少竞争的场景,比如某些初始化操作。
- 不适合高竞争环境,因为一旦有其他线程竞争,偏向锁会升级为轻量级锁,反而增加开销。
三、轻量级锁:应对低竞争环境
如果偏向锁检测到有竞争(比如第二个线程尝试获取锁),JVM 会撤销偏向锁,并升级为轻量级锁。轻量级锁的核心思想是:通过 CAS(Compare-And-Swap)操作来避免直接使用操作系统层面的互斥锁,从而减少开销。
示例:轻量级锁的工作机制
public class LightweightLockExample {
public static void main(String[] args) {
Object lock = new Object();
// 线程1 获取锁
new Thread(() -> {
synchronized (lock) {
System.out.println("线程1 持有锁");
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 线程2 尝试获取锁,触发轻量级锁
new Thread(() -> {
synchronized (lock) {
System.out.println("线程2 获取锁成功");
}
}).start();
}
}
注释说明:
- 线程1 先获取锁,此时 JVM 可能使用偏向锁(如果没有禁用偏向锁)。
- 线程2 尝试获取锁时,JVM 检测到竞争,撤销偏向锁,升级为轻量级锁。
- 轻量级锁通过 CAS 操作竞争锁,如果竞争失败,会短暂自旋(忙等待),而不是直接阻塞。
适用场景
- 适用于低竞争环境,比如少量线程交替获取锁。
- 如果竞争激烈,轻量级锁会升级为重量级锁,此时自旋会浪费 CPU 资源。
四、锁的升级过程
JVM 的锁优化是一个动态过程,通常遵循以下顺序:
- 无锁:对象刚创建时,没有任何线程持有锁。
- 偏向锁:当第一个线程获取锁时,JVM 启用偏向锁。
- 轻量级锁:如果检测到竞争,偏向锁升级为轻量级锁。
- 重量级锁:如果轻量级锁竞争激烈(比如自旋失败),JVM 会升级为重量级锁,此时线程会进入阻塞状态。
示例:锁升级的完整过程
public class LockUpgradeExample {
public static void main(String[] args) {
Object lock = new Object();
// 阶段1:偏向锁
synchronized (lock) {
System.out.println("阶段1:偏向锁生效");
}
// 阶段2:轻量级锁(模拟第二个线程竞争)
new Thread(() -> {
synchronized (lock) {
System.out.println("阶段2:轻量级锁生效");
}
}).start();
// 阶段3:重量级锁(模拟高竞争)
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (lock) {
System.out.println("阶段3:重量级锁生效");
}
}).start();
}
}
}
注释说明:
- 初始阶段,锁是偏向锁。
- 第二个线程竞争时,升级为轻量级锁。
- 多个线程竞争时,轻量级锁无法满足需求,最终升级为重量级锁。
五、技术优缺点与注意事项
优点
- 偏向锁:无竞争时性能极高,几乎无额外开销。
- 轻量级锁:低竞争时比重量级锁更高效,减少线程阻塞。
缺点
- 偏向锁:在竞争场景下,撤销偏向锁会带来额外开销。
- 轻量级锁:自旋会消耗 CPU 资源,高竞争时不如直接使用重量级锁。
注意事项
- 可以通过 JVM 参数
-XX:-UseBiasedLocking禁用偏向锁。 - 在高并发场景下,可以考虑使用更高级的并发工具(如
ReentrantLock)。
六、总结
偏向锁和轻量级锁是 JVM 针对低竞争场景的优化手段,可以有效减少锁带来的性能开销。但在高并发环境下,它们可能适得其反,因此需要根据实际场景选择合适的同步策略。
评论