显式锁 Lock 与 ReentrantLock
本教程共 100 篇 · 第 99 篇 · 更新于 2026-08-05 · 约 15 分钟阅读
99. 显式锁 Lock 与 ReentrantLock
本节目标:掌握 ReentrantLock 的标准用法,理解它相比 synchronized 多出来的那些能力。
synchronized 差在哪
synchronized 简单可靠,但有几个绕不过去的限制。
拿不到锁只能死等。一旦进入 BLOCKED,就没有任何退路,不能设超时,也不能放弃。
不能被中断。等锁的线程调 interrupt() 没有任何反应,只能一直等下去。
无法知道是否成功。没有「试着拿一下,拿不到就先做别的」这种选项。
加锁范围受语法约束。必须在同一个代码块里加锁和解锁,无法在方法 A 加锁、方法 B 解锁。
Java 5 引入的 java.util.concurrent.locks 包,用显式锁补齐了这些能力。
Lock 接口
Lock 是一个接口,定义了锁的基本行为:
| 方法 | 说明 |
|---|---|
lock() | 获取锁,拿不到就一直等 |
tryLock() | 尝试获取,立即返回 true 或 false |
tryLock(time, unit) | 限时尝试,超时返回 false |
lockInterruptibly() | 获取锁,等待期间可被中断 |
unlock() | 释放锁 |
最常用的实现类是 ReentrantLock,名字里的 Reentrant 就是「可重入」,这一点和 synchronized 一致。
标准写法
显式锁最大的特点是必须手动解锁。忘了解锁,其他线程就永远进不来。
所以有一套雷打不动的写法:
import java.util.concurrent.locks.*;
public class LockCounter {
private static int count = 0;
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) {
lock.lock(); // 加锁写在 try 之前
try {
count++;
} finally {
lock.unlock(); // 解锁必须在 finally 里
}
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("结果:" + count);
}
}
```text
输出:
```text
结果:200000
两个细节值得强调。
lock() 要写在 try 外面。如果写在里面,万一加锁本身失败抛了异常,finally 里的 unlock() 会对一把没拿到的锁解锁,抛出 IllegalMonitorStateException,掩盖真正的错误。
unlock() 必须写在 finally 里。临界区代码一旦抛异常,没有 finally 就跳过解锁了,这把锁永远释放不掉,所有等待的线程全部卡死。
Warning这是显式锁最大的风险点。
synchronized由 JVM 保证退出代码块时自动释放锁,哪怕抛异常也一样。用Lock就得自己扛这个责任,try-finally 一个都不能少。
优势一:tryLock 不死等
tryLock() 尝试获取锁,成功返回 true,失败立刻返回 false,绝不阻塞。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.*;
public class TryLockDemo {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
Runnable task = () -> {
String name = Thread.currentThread().getName();
try {
// 最多等 500 毫秒
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
System.out.println(name + " 拿到锁,开始工作");
Thread.sleep(1000);
} finally {
lock.unlock();
System.out.println(name + " 释放锁");
}
} else {
System.out.println(name + " 等待超时,改做别的事");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
new Thread(task, "线程1").start();
new Thread(task, "线程2").start();
}
}
```text
输出:
```text
线程1 拿到锁,开始工作
线程2 等待超时,改做别的事
线程1 释放锁
线程2 没有傻等,超时后走了另一条路径。
这个能力在实际开发中很有用。用户点了按钮,后台任务正忙,与其让界面卡住,不如立刻提示「系统繁忙,请稍后重试」。
tryLock 还是防死锁的利器。上一节转账的例子,改成「两把锁都 tryLock 成功才继续,否则全部释放后重试」,就能从根本上避免死锁。
优势二:可中断
lockInterruptibly() 在等待锁的过程中响应中断:
import java.util.concurrent.locks.*;
public class InterruptibleLock {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread holder = new Thread(() -> {
lock.lock();
try {
Thread.sleep(5000); // 长时间占着锁
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}, "持锁线程");
Thread waiter = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可被打断的等待
try {
System.out.println("拿到锁");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("等锁时被中断,放弃等待");
}
}, "等锁线程");
holder.start();
Thread.sleep(100);
waiter.start();
Thread.sleep(500);
waiter.interrupt(); // 打断等待
}
}
```text
输出:
```text
等锁时被中断,放弃等待
换成 synchronized,waiter 会一直等满 5 秒,interrupt() 完全无效。
优势三:公平锁
ReentrantLock 构造时可以传一个布尔值,选择公平模式:
Lock fair = new ReentrantLock(true); // 公平锁
Lock unfair = new ReentrantLock(); // 非公平锁(默认)
```bash
**非公平锁**:锁释放时,所有等待线程一起抢,谁抢到算谁的。新来的线程可能插队,排了很久的线程反而抢不到。
**公平锁**:严格按申请顺序排队,先到先得,不会有线程被饿死。
听起来公平锁更好,但默认是非公平的,原因是**性能**。
非公平锁允许刚释放锁的线程立刻再次获得它,省掉了唤醒队列中其他线程的开销。线程唤醒和上下文切换的成本很高,减少这个动作带来的吞吐提升明显。
> [!TIP]
> 除非确实观察到某些线程长期抢不到锁,否则用默认的非公平锁。公平锁的吞吐量通常明显更低。
## Condition:精准唤醒
`synchronized` 配套的是 `wait()`/`notify()`,只有一个等待队列。调 `notifyAll()` 会把所有等待线程都叫醒,让它们重新竞争,很浪费。
`Lock` 配套的是 `Condition`,一把锁可以创建多个条件队列,实现精准唤醒:
```java
import java.util.concurrent.locks.*;
public class ConditionDemo {
private static final Lock lock = new ReentrantLock();
private static final Condition notEmpty = lock.newCondition();
private static int data = 0;
public static void main(String[] args) throws InterruptedException {
Thread consumer = new Thread(() -> {
lock.lock();
try {
while (data == 0) {
notEmpty.await(); // 等待并释放锁
}
System.out.println("消费到数据:" + data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
});
consumer.start();
Thread.sleep(300);
lock.lock();
try {
data = 42;
notEmpty.signal(); // 唤醒等待该条件的线程
} finally {
lock.unlock();
}
consumer.join();
}
}
输出:
消费到数据:42
```bash
`await()` 对应 `wait()`,`signal()` 对应 `notify()`,`signalAll()` 对应 `notifyAll()`。
生产者消费者模型中,可以建两个 `Condition`:`notFull` 让生产者等,`notEmpty` 让消费者等。队列满时只唤醒消费者,空时只唤醒生产者,互不打扰。
注意 `await()` 必须写在 `while` 循环里检查条件,不能用 `if`。线程被唤醒后条件未必真的满足(可能被别的线程抢先消费了),必须重新检查。
## 读写锁
`ReentrantReadWriteLock` 是另一个实用工具,它把锁拆成读锁和写锁:
- 多个线程可以**同时持有读锁**
- 写锁是**独占**的,与读锁和其他写锁都互斥
```java
import java.util.concurrent.locks.*;
import java.util.*;
public class CacheDemo {
private final Map<String, String> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public String get(String key) {
rwLock.readLock().lock(); // 读锁:多线程可并发
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, String value) {
rwLock.writeLock().lock(); // 写锁:独占
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
读多写少的场景(配置中心、本地缓存、字典表)用它收益很大。所有读操作并发执行,只有写的时候才互斥。
反过来,写操作频繁时读写锁反而更慢——维护两套状态本身有开销,还不如直接用 ReentrantLock。
该用哪个
| 场景 | 选择 |
|---|---|
| 简单的互斥保护 | synchronized |
| 需要超时或尝试获取 | ReentrantLock + tryLock |
| 等锁期间需要能中断 | ReentrantLock + lockInterruptibly |
| 需要多个等待队列 | ReentrantLock + Condition |
| 读多写少 | ReentrantReadWriteLock |
日常开发优先用 synchronized。它写法简洁,不会忘记解锁,而且现代 JVM 对它做了大量优化(偏向锁、轻量级锁、锁消除),性能和 ReentrantLock 基本持平。
只有确实需要那些额外能力时,才换成显式锁。
本节小结
Lock是接口,ReentrantLock是最常用的实现,同样可重入。- 标准写法:
lock()在 try 外,unlock()在 finally 内,一个都不能少。 tryLock()不阻塞,带超时版本可以等一会儿再放弃。lockInterruptibly()让等锁的线程能响应中断。- 默认非公平锁性能更好,公平锁只在防饥饿时使用。
Condition支持多个等待队列,实现精准唤醒。await()必须放在while循环里重新检查条件。- 读写锁适合读多写少,写频繁时反而更慢。
- 没有特殊需求就用
synchronized,更安全也更简洁。