Bootstrap

Java面试要点93 - Java中的偏向锁、轻量级锁、重量级锁

在这里插入图片描述


一、引言

在Java并发编程中,锁是保证线程安全的重要机制。为了提升synchronized的性能,JDK 1.6引入了锁升级的概念,包括偏向锁、轻量级锁和重量级锁。

二、对象头与锁

在深入理解Java的锁机制之前,我们需要先了解Java对象头的结构。在HotSpot虚拟机中,对象头包含两部分信息:Mark Word和类型指针。Mark Word用于存储对象的运行时数据,如哈希码、GC分代年龄、锁状态标志等。在不同的锁状态下,Mark Word中存储的内容也不同。

public class ObjectHeaderDemo {
    private static Object lock = new Object();
    
    public static void main(String[] args) {
        System.out.println("Before synchronized block - Object in normal state");
        
        synchronized(lock) {
            System.out.println("Inside synchronized block - Object header may change");
            // 根据线程竞争情况,对象头会发生变化
        }
        
        System.out.println("After synchronized block - Object header returns to normal");
    }
}

三、偏向锁

偏向锁是JVM为了减少无竞争情况下锁的开销而引入的优化机制。当一个线程第一次获取锁时,会在对象头记录该线程的ID。之后该线程再次获取锁时,只需要比较线程ID即可,不需要做同步操作。

3.1 偏向锁的获取过程

public class BiasedLockExample {
    private static Object lock = new Object();
    private int count = 0;
    
    public void incrementWithBiasedLock() {
        synchronized(lock) {
            count++;
            // 同一个线程多次进入,只需要比对线程ID
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        BiasedLockExample example = new BiasedLockExample();
        // 单线程重复获取锁,触发偏向锁
        for(int i = 0; i < 10; i++) {
            example.incrementWithBiasedLock();
        }
    }
}

3.2 偏向锁的撤销过程

public class BiasedLockRevocation {
    private static final Object lock = new Object();
    
    public static void main(String[] args) throws InterruptedException {
        // 第一个线程,获取偏向锁
        Thread t1 = new Thread(() -> {
            synchronized(lock) {
                System.out.println("Thread 1: Acquired biased lock");
                try {
                    // 模拟持有锁的操作
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        t1.start();
        t1.join(); // 等待t1执行完成
        
        // 第二个线程,触发偏向锁撤销
        Thread t2 = new Thread(() -> {
            synchronized(lock) {
                System.out.println("Thread 2: Triggered biased lock revocation");
                // 这时候已经升级为轻量级锁
            }
        });
        
        // 添加延迟,确保偏向锁已经建立
        Thread.sleep(50);
        t2.start();
    }
}

四、轻量级锁

当有其他线程尝试获取锁时,偏向锁就会升级为轻量级锁。轻量级锁采用CAS操作和自旋来避免线程进入内核态。

4.1 轻量级锁的加锁过程

public class LightweightLockExample {
    private static final Object lock = new Object();
    private static int counter = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> increment(1));
        Thread t2 = new Thread(() -> increment(2));
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
    }
    
    private static void increment(int threadId) {
        for(int i = 0; i < 100; i++) {
            synchronized(lock) {
                counter++;
                System.out.println("Thread " + threadId + ": " + counter);
                // 短时间操作,适合轻量级锁
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4.2 轻量级锁的自旋过程

public class SpinLockDemo {
    private final AtomicReference<Thread> owner = new AtomicReference<>();
    private static int sharedResource = 0;
    
    public void lock() {
        Thread current = Thread.currentThread();
        // 自旋等待,模拟轻量级锁的CAS操作
        while (!owner.compareAndSet(null, current)) {
            System.out.println(current.getName() + " is spinning");
            // 自旋等待,不放弃CPU
            Thread.yield();
        }
    }
    
    public void unlock() {
        Thread current = Thread.currentThread();
        owner.compareAndSet(current, null);
    }
    
    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo spinLock = new SpinLockDemo();
        Runnable task = () -> {
            spinLock.lock();
            try {
                sharedResource++;
                Thread.sleep(100); // 模拟短时间操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinLock.unlock();
            }
        };
        
        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("Final value: " + sharedResource);
    }
}

五、重量级锁

当轻量级锁自旋次数过多或等待线程数量过多时,锁会升级为重量级锁。重量级锁会导致线程阻塞和唤醒,需要操作系统介入。

5.1 重量级锁的实现原理

public class HeavyweightLockExample {
    private static final Object lock = new Object();
    private static final int THREAD_COUNT = 5;
    
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
        
        for(int i = 0; i < THREAD_COUNT; i++) {
            executor.submit(() -> {
                synchronized(lock) {
                    // 模拟长时间操作,导致锁升级
                    try {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + " is executing");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        
        executor.shutdown();
    }
}

5.2 重量级锁的等待队列

public class HeavyweightLockQueue {
    private static final Object lock = new Object();
    private static final int THREAD_COUNT = 5;
    
    static class Task implements Runnable {
        private final int id;
        
        public Task(int id) {
            this.id = id;
        }
        
        @Override
        public void run() {
            long startTime = System.currentTimeMillis();
            synchronized(lock) {
                long waitTime = System.currentTimeMillis() - startTime;
                System.out.printf("Thread %d acquired lock after waiting %d ms%n", id, waitTime);
                
                try {
                    // 模拟任务执行
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                System.out.printf("Thread %d finished execution%n", id);
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREAD_COUNT];
        
        // 创建并启动多个线程
        for(int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(new Task(i));
            threads[i].start();
        }
        
        // 等待所有线程完成
        for(Thread thread : threads) {
            thread.join();
        }
    }
}

六、锁升级的最佳实践

在实际开发中,需要根据不同的场景选择合适的锁策略。对于只有单线程访问的同步块,偏向锁能够提供最好的性能。对于线程交替执行的场景,轻量级锁的自旋等待更有优势。而对于锁竞争激烈的场景,重量级锁能够避免CPU资源的浪费。

此外,还可以通过JVM参数来调整锁的行为:

  • -XX:+UseBiasedLocking:开启偏向锁
  • -XX:-UseBiasedLocking:关闭偏向锁
  • -XX:BiasedLockingStartupDelay=0:设置偏向锁启动延迟
  • -XX:PreBlockSpin:设置自旋次数

总结

Java中的锁升级机制是JVM对synchronized的重要优化。通过偏向锁和轻量级锁,大大提升了synchronized在不同场景下的性能表现。理解这三种锁的原理和适用场景,对于编写高性能的并发程序至关重要。在实际开发中,应该根据具体的业务场景选择合适的锁实现,并通过必要的性能测试来验证锁的效果。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;