锁机制
锁是实现线程同步的核心机制。JUC 提供了比 synchronized 更灵活、更强大的锁实现,本章将深入讲解这些锁机制。
synchronized 的局限性
在介绍 JUC 的锁之前,我们先理解为什么需要更强大的锁机制。synchronized 关键字虽然使用简单,但存在以下局限:
无法中断等待
当一个线程尝试获取 synchronized 锁时,如果锁被其他线程持有,它只能无限期等待,无法被中断。
public class SynchronizedLimitation {
private final Object lock = new Object();
public void method1() {
synchronized (lock) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2() {
synchronized (lock) {
System.out.println("获取锁成功");
}
}
}
public static void main(String[] args) throws Exception {
SynchronizedLimitation demo = new SynchronizedLimitation();
Thread t1 = new Thread(() -> demo.method1());
Thread t2 = new Thread(() -> demo.method2());
t1.start();
Thread.sleep(100);
t2.start();
Thread.sleep(1000);
t2.interrupt();
System.out.println("t2 已被中断,但仍会等待锁");
t1.join();
t2.join();
}
上面的代码中,即使 t2 线程被中断,它仍然会继续等待 synchronized 锁。
无法设置超时
synchronized 不支持超时获取锁,如果锁被长时间持有,其他线程只能一直等待。
不支持公平锁
synchronized 是非公平的,新来的线程可能比等待更久的线程先获取锁,这可能导致某些线程"饥饿"。
锁的状态不可查询
无法判断锁是否被持有、有多少线程在等待等信息。
Lock 接口
JUC 的 Lock 接口定义了比 synchronized 更灵活的锁操作:
public interface Lock {
void lock(); // 获取锁
void lockInterruptibly() throws InterruptedException; // 可中断地获取锁
boolean tryLock(); // 尝试获取锁(立即返回)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时获取锁
void unlock(); // 释放锁
Condition newCondition(); // 创建条件变量
}
Lock 接口的核心优势:
| 特性 | synchronized | Lock |
|---|---|---|
| 中断等待 | 不支持 | 支持 lockInterruptibly() |
| 超时获取 | 不支持 | 支持 tryLock(timeout) |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件变量 | 单一条件 | 多个 Condition |
| 锁状态查询 | 不支持 | 支持 |
ReentrantLock
ReentrantLock 是 Lock 接口最常用的实现,它是一种可重入锁。"可重入"意味着同一个线程可以多次获取同一把锁而不会死锁。
基本使用
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
System.out.println(Thread.currentThread().getName() + ": count = " + count);
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
demo.increment();
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
demo.increment();
}
}, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终 count = " + demo.count);
}
}
重要: lock() 和 unlock() 必须成对出现,unlock() 应该放在 finally 块中确保一定执行,否则可能导致死锁。
可重入性验证
public class ReentrantDemo {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println("outer 方法获取锁");
inner();
} finally {
lock.unlock();
System.out.println("outer 方法释放锁");
}
}
public void inner() {
lock.lock();
try {
System.out.println("inner 方法获取锁(重入)");
} finally {
lock.unlock();
System.out.println("inner 方法释放锁");
}
}
public static void main(String[] args) {
ReentrantDemo demo = new ReentrantDemo();
demo.outer();
}
}
输出:
outer 方法获取锁
inner 方法获取锁(重入)
inner 方法释放锁
outer 方法释放锁
同一个线程在持有锁的情况下,可以再次获取同一把锁,这就是可重入性。
公平锁与非公平锁
ReentrantLock 支持公平锁和非公平锁两种模式:
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁(默认)
公平锁:按照线程请求锁的顺序获取锁,先来先得。
非公平锁:允许"插队",新来的线程可能比等待队列中的线程先获取锁。
public class FairVsUnfair {
private static class Runner implements Runnable {
private final ReentrantLock lock;
private final String name;
Runner(ReentrantLock lock, String name) {
this.lock = lock;
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
lock.lock();
try {
System.out.println(name + " 获取锁,第 " + (i + 1) + " 次");
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) {
System.out.println("=== 非公平锁 ===");
testLock(new ReentrantLock(false));
System.out.println("\n=== 公平锁 ===");
testLock(new ReentrantLock(true));
}
private static void testLock(ReentrantLock lock) {
Thread[] threads = new Thread[3];
for (int i = 0; i < 3; i++) {
threads[i] = new Thread(new Runner(lock, "Thread-" + (i + 1)));
}
for (Thread t : threads) t.start();
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
非公平锁的输出可能显示某个线程连续多次获取锁,而公平锁的输出会严格按照请求顺序交替获取。
性能对比:非公平锁性能通常优于公平锁,因为公平锁需要维护队列,线程切换开销更大。在大多数场景下,推荐使用非公平锁(默认)。
tryLock - 尝试获取锁
tryLock 方法提供非阻塞和超时两种获取锁的方式:
public class TryLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void tryLockExample() {
if (lock.tryLock()) {
try {
System.out.println("获取锁成功,执行任务");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败,执行其他任务");
}
}
public void tryLockWithTimeout() throws InterruptedException {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
System.out.println("在超时时间内获取锁成功");
} finally {
lock.unlock();
}
} else {
System.out.println("超时仍未获取锁");
}
}
public static void main(String[] args) throws InterruptedException {
TryLockDemo demo = new TryLockDemo();
Thread t1 = new Thread(() -> {
demo.lock.lock();
try {
System.out.println("t1 获取锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
demo.lock.unlock();
System.out.println("t1 释放锁");
}
});
Thread t2 = new Thread(() -> demo.tryLockExample());
Thread t3 = new Thread(() -> {
try {
demo.tryLockWithTimeout();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(100);
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
tryLock 的典型应用场景:
- 避免死锁:如果获取不到锁就放弃或执行其他逻辑
- 限时等待:设置合理的超时时间,避免无限等待
- 快速失败:检测锁是否可用,不可用立即返回
lockInterruptibly - 可中断获取锁
public class InterruptibleLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void holdLock() {
lock.lock();
try {
System.out.println("线程1获取锁,持有5秒");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void interruptibleLock() {
try {
System.out.println("线程2尝试获取锁(可中断)...");
lock.lockInterruptibly();
try {
System.out.println("线程2获取锁成功");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("线程2被中断,放弃获取锁");
}
}
public static void main(String[] args) throws InterruptedException {
InterruptibleLockDemo demo = new InterruptibleLockDemo();
Thread t1 = new Thread(() -> demo.holdLock());
Thread t2 = new Thread(() -> demo.interruptibleLock());
t1.start();
Thread.sleep(100);
t2.start();
Thread.sleep(1000);
System.out.println("主线程中断 t2");
t2.interrupt();
t1.join();
t2.join();
}
}
输出:
线程1获取锁,持有5秒
线程2尝试获取锁(可中断)...
主线程中断 t2
线程2被中断,放弃获取锁
线程1释放锁
lockInterruptibly 允许在等待锁的过程中响应中断,这是与 synchronized 的重要区别。
Condition - 条件变量
Condition 提供了比 wait/notify 更灵活的线程通信机制。一个 Lock 可以关联多个 Condition,实现更精细的线程协调。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items = new Object[10];
private int putIndex, takeIndex, count;
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
System.out.println("队列已满,生产者等待...");
notFull.await();
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
count++;
System.out.println("生产: " + item + ", 当前数量: " + count);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
System.out.println("队列为空,消费者等待...");
notEmpty.await();
}
Object item = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
count--;
System.out.println("消费: " + item + ", 当前数量: " + count);
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ConditionDemo buffer = new ConditionDemo();
Thread producer = new Thread(() -> {
for (int i = 0; i < 15; i++) {
try {
buffer.put("Item-" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 15; i++) {
try {
buffer.take();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
consumer.start();
}
}
Condition 的核心方法:
| 方法 | 说明 |
|---|---|
| await() | 释放锁并等待,被 signal 唤醒后重新获取锁 |
| awaitUninterruptibly() | 不可中断的等待 |
| awaitNanos(long) | 超时等待(纳秒) |
| await(long, TimeUnit) | 超时等待 |
| signal() | 唤醒一个等待线程 |
| signalAll() | 唤醒所有等待线程 |
ReentrantReadWriteLock
ReentrantReadWriteLock 是读写锁的实现,它将锁分为读锁和写锁:
- 读锁(共享锁):多个线程可以同时持有读锁
- 写锁(排他锁):只有一个线程可以持有写锁,且此时不能有读锁
这种设计在读多写少的场景下能显著提高并发性能。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private String data = "初始数据";
public String read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始读取");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " 读取完成: " + data);
return data;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
readLock.unlock();
}
}
public void write(String newData) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 开始写入");
Thread.sleep(1000);
data = newData;
System.out.println(Thread.currentThread().getName() + " 写入完成: " + newData);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
Thread r1 = new Thread(() -> demo.read(), "Reader-1");
Thread r2 = new Thread(() -> demo.read(), "Reader-2");
Thread r3 = new Thread(() -> demo.read(), "Reader-3");
Thread w1 = new Thread(() -> demo.write("新数据1"), "Writer-1");
r1.start();
r2.start();
r3.start();
w1.start();
}
}
输出显示三个读线程可以同时执行,而写线程需要等待所有读线程完成后才能执行。
读写锁的规则:
| 当前状态 | 读锁请求 | 写锁请求 |
|---|---|---|
| 无锁 | 成功 | 成功 |
| 读锁持有 | 成功 | 等待 |
| 写锁持有 | 等待 | 等待 |
锁降级:写锁可以降级为读锁,但读锁不能升级为写锁。
public void lockDowngrade() {
writeLock.lock();
try {
data = "更新数据";
readLock.lock();
} finally {
writeLock.unlock();
}
try {
System.out.println("锁已降级为读锁: " + data);
} finally {
readLock.unlock();
}
}
StampedLock
StampedLock 是 Java 8 引入的锁,它提供了乐观读锁的支持,性能比 ReentrantReadWriteLock 更好。
import java.util.concurrent.locks.StampedLock;
public class StampedLockDemo {
private final StampedLock sl = new StampedLock();
private double x, y;
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
public void moveIfAtOrigin(double newX, double newY) {
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
public static void main(String[] args) {
StampedLockDemo point = new StampedLockDemo();
point.move(3, 4);
System.out.println("距离原点: " + point.distanceFromOrigin());
point.moveIfAtOrigin(0, 0);
System.out.println("移动后距离: " + point.distanceFromOrigin());
}
}
StampedLock 的三种模式:
| 模式 | 说明 |
|---|---|
| 写锁 | 排他锁,与 ReentrantReadWriteLock 类似 |
| 悲观读锁 | 共享锁,与 ReentrantReadWriteLock 类似 |
| 乐观读锁 | 无锁读取,通过版本号验证数据是否被修改 |
乐观读锁的优势在于不需要真正获取锁,只是读取数据并记录版本号,最后验证版本号是否变化。如果没有变化,说明数据一致,可以继续使用;如果变化了,再升级为悲观读锁重新读取。
锁的选择指南
| 场景 | 推荐锁 | 原因 |
|---|---|---|
| 简单同步 | synchronized | 使用简单,JVM 自动管理 |
| 需要中断/超时 | ReentrantLock | 支持可中断和超时获取 |
| 公平性要求 | ReentrantLock(true) | 支持公平锁 |
| 读多写少 | ReentrantReadWriteLock | 读锁共享,提高并发 |
| 高性能读 | StampedLock | 乐观读,性能最优 |
| 多条件等待 | ReentrantLock + Condition | 支持多个 Condition |
小结
本章介绍了 JUC 提供的锁机制:
- Lock 接口:提供了比 synchronized 更灵活的锁操作,包括可中断、超时、公平性等特性
- ReentrantLock:可重入锁,是最常用的 Lock 实现,支持公平/非公平模式
- Condition:条件变量,提供比 wait/notify 更灵活的线程通信
- ReentrantReadWriteLock:读写锁,适合读多写少的场景
- StampedLock:Java 8 引入,支持乐观读,性能更优
选择合适的锁对于并发程序的性能至关重要。在简单场景下,synchronized 仍然是不错的选择;在需要更多控制时,ReentrantLock 提供了更大的灵活性;在读多写少的场景下,读写锁能显著提高并发性能。