概述

Java 中的 Lock 是一个接口,用于控制多个线程对共享资源的访问。与块结构化的 synchronized 相比,它允许在不同作用域中获取和释放锁,并提供非阻塞、可中断和带超时的获取方式。
Lock 只定义协议,不保证所有实现都可重入、独占或基于 AQS。本文以最常用的 ReentrantLock 为主要实现说明;读写锁等实现可能允许多个线程并发持有读锁。所有 Lock 实现的成功获取和释放操作,都必须提供与内置监视器锁相同的内存同步语义。
Lock 接口的核心方法
接口定义可以简化为:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
void unlock();
Condition newCondition();
}
lock():阻塞式获取锁
基本用法:
private final Lock lock = new ReentrantLock();
public void update() {
lock.lock();
try {
// 访问或修改共享数据
} finally {
lock.unlock();
}
}
ReentrantLock 是可重入锁:当前线程已经持有锁时,可以再次获取它。每成功获取一次,都必须对应释放一次。
lock.lock(); // 第一次,持有次数为 1
lock.lock(); // 第二次,持有次数为 2
try {
// 临界区
} finally {
lock.unlock(); // 持有次数变为 1
lock.unlock(); // 持有次数变为 0,真正释放
}
在当前 JDK 的 ReentrantLock 实现中,获取失败的线程会通过 AQS 进入同步等待队列;这属于 ReentrantLock 的实现细节,不是 Lock 接口对所有实现的要求。
ReentrantLock.lock() 不声明 InterruptedException,因此线程不能通过中断取消这次锁等待。如果等待期间收到中断,它仍会继续尝试获取锁;成功返回后,中断状态会被保留。
public void doSomething() {
lock.lock(); // 排队时被中断,仍会继续等待,直到成功获取锁
try {
// 如果等待锁时收到过中断,此时中断状态仍为 true。
// sleep() 会立即检测该状态并抛出 InterruptedException。
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("收到中断信号,开始处理善后工作...");
} finally {
lock.unlock();
}
}
lockInterruptibly():可中断地获取锁
方法定义:
void lockInterruptibly() throws InterruptedException;
基本用法:
public void update() throws InterruptedException {
lock.lockInterruptibly();
try {
doSomething();
} finally {
lock.unlock();
}
}
它与 lock() 的区别是:线程等待锁时
lock()
被中断 → 继续等待锁
lockInterruptibly()
被中断 → 放弃等待并抛出 InterruptedException
对于 ReentrantLock,如果线程调用前中断状态已经为 true,会立即抛出 InterruptedException,而不是继续获取锁。
Thread.currentThread().interrupt();
lock.lockInterruptibly();
Thread.sleep() 和 ReentrantLock.lockInterruptibly() 抛出 InterruptedException 时,都会清除当前线程的中断状态。上层如果不能完成中断处理,通常需要重新设置中断状态或继续抛出异常。
tryLock():非阻塞尝试获取锁
方法定义:
boolean tryLock();
它会立即尝试获取锁,不会进入长时间等待。
if (lock.tryLock()) {
try {
doSomething();
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败");
}
即使 ReentrantLock 使用公平策略,无参 tryLock() 仍会在锁恰好可用时直接获取,可能绕过已经排队的线程。如果必须遵守公平策略,可以使用可中断的 tryLock(0, TimeUnit.SECONDS)。
tryLock(time, unit):超时获取锁
方法定义:
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
它介于 lock() 和 tryLock() 之间:
lock() 一直等待
tryLock() 完全不等待
tryLock(time, unit) 最多等待指定时间
带超时的 tryLock 同样可以响应中断。对于公平 ReentrantLock,它会遵守公平策略,不会在已有线程排队时直接插队。
unlock():释放锁
方法定义:
void unlock();
unlock() 不只是把一个布尔值改成“未锁定”。对于 ReentrantLock,它大致执行:
检查当前线程是否为锁持有者
↓
重入计数减 1
↓
计数是否为 0?
├─ 否:仍然持有锁
└─ 是:清除持有者并唤醒等待线程
如果当前线程并未持有该 ReentrantLock,调用 unlock() 会抛出 IllegalMonitorStateException。
这里也体现了锁和二值信号量的一个区别:
- 锁具有所有权:谁成功获取,谁负责释放
- 信号量维护的是许可数量,没有线程所有权;一个线程可以释放由另一个线程获取的许可
newCondition():创建条件队列
方法定义:
Condition newCondition();
它不会创建另一把锁,而是基于当前锁创建一个条件等待队列:
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
一把锁可以创建多个 Condition:
同一把锁
├─ notEmpty 条件队列
└─ notFull 条件队列
这比 synchronized 的单一等待集合更灵活。
调用 Condition.await()、signal() 或 signalAll() 时,当前线程必须持有关联的锁。await() 会原子地释放锁并进入条件队列;线程被唤醒后,还必须重新获取关联锁,成功后才能从 await() 返回。
条件等待允许出现虚假唤醒,因此判断条件时应使用 while,而不是只检查一次的 if:
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
consume(queue.remove());
} finally {
lock.unlock();
}