一、为什么需要锁优化

多线程编程中,锁是保证线程安全的重要手段,但锁的使用往往会带来性能开销。比如,当多个线程竞争同一个锁时,会导致线程阻塞,进而影响程序的整体吞吐量。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("同一个线程再次获取锁,偏向锁生效");
        }
    }
}

注释说明:

  1. 第一次进入同步块时,JVM 会检测到当前没有竞争,于是启用偏向锁。
  2. 后续同一个线程再次进入同步块时,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. 线程1 先获取锁,此时 JVM 可能使用偏向锁(如果没有禁用偏向锁)。
  2. 线程2 尝试获取锁时,JVM 检测到竞争,撤销偏向锁,升级为轻量级锁。
  3. 轻量级锁通过 CAS 操作竞争锁,如果竞争失败,会短暂自旋(忙等待),而不是直接阻塞。

适用场景

  • 适用于低竞争环境,比如少量线程交替获取锁。
  • 如果竞争激烈,轻量级锁会升级为重量级锁,此时自旋会浪费 CPU 资源。

四、锁的升级过程

JVM 的锁优化是一个动态过程,通常遵循以下顺序:

  1. 无锁:对象刚创建时,没有任何线程持有锁。
  2. 偏向锁:当第一个线程获取锁时,JVM 启用偏向锁。
  3. 轻量级锁:如果检测到竞争,偏向锁升级为轻量级锁。
  4. 重量级锁:如果轻量级锁竞争激烈(比如自旋失败),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();
        }
    }
}

注释说明:

  1. 初始阶段,锁是偏向锁。
  2. 第二个线程竞争时,升级为轻量级锁。
  3. 多个线程竞争时,轻量级锁无法满足需求,最终升级为重量级锁。

五、技术优缺点与注意事项

优点

  • 偏向锁:无竞争时性能极高,几乎无额外开销。
  • 轻量级锁:低竞争时比重量级锁更高效,减少线程阻塞。

缺点

  • 偏向锁:在竞争场景下,撤销偏向锁会带来额外开销。
  • 轻量级锁:自旋会消耗 CPU 资源,高竞争时不如直接使用重量级锁。

注意事项

  1. 可以通过 JVM 参数 -XX:-UseBiasedLocking 禁用偏向锁。
  2. 在高并发场景下,可以考虑使用更高级的并发工具(如 ReentrantLock)。

六、总结

偏向锁和轻量级锁是 JVM 针对低竞争场景的优化手段,可以有效减少锁带来的性能开销。但在高并发环境下,它们可能适得其反,因此需要根据实际场景选择合适的同步策略。