文章目录
一、引言
在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在不同场景下的性能表现。理解这三种锁的原理和适用场景,对于编写高性能的并发程序至关重要。在实际开发中,应该根据具体的业务场景选择合适的锁实现,并通过必要的性能测试来验证锁的效果。