深入理解 Java 可重入锁(ReentrantLock)
一、什么是可重入锁?
可重入锁(Reentrant Lock),顾名思义,指的是同一个线程能够多次获取同一把锁而不会发生死锁。换句话说,如果一个线程已经持有了某个锁,当它再次尝试获取该锁时,依然能够成功获取,而不会被自己阻塞。
可重入性的本质
锁的"可重入性"解决了一个关键问题:当线程在持有锁的情况下递归调用或调用其他需要同一锁的同步方法时,不会导致死锁。
public class NonReentrantExample {
private boolean locked = false;
// 一个不可重入的"锁"实现
public synchronized void outerMethod() {
System.out.println("进入 outerMethod");
innerMethod(); // 如果是不可重入锁,这里会死锁!
}
public synchronized void innerMethod() {
System.out.println("进入 innerMethod");
}
}
在 Java 中,synchronized 和 ReentrantLock 都是可重入的。上面的代码中,线程调用 outerMethod() 后获取了锁,然后在方法内部调用 innerMethod() 时,因为锁已经被自己持有,所以能够顺利再次获取——这就是可重入。
不可重入锁的问题
如果锁不可重入,会发生什么?
class NonReentrantLock {
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException {
while (isLocked) {
wait();
}
isLocked = true;
}
public synchronized void unlock() {
isLocked = false;
notify();
}
}
用上面这个不可重入锁执行 outerMethod() → innerMethod(),线程在 innerMethod 中调用 lock() 时,isLocked 仍然为 true(因为自己还没释放),线程就会永远等待下去——触发死锁。这正说明可重入机制的重要性。
二、ReentrantLock 类概述
ReentrantLock 是 java.util.concurrent.locks 包下提供的一个显式锁实现,自 Java 5 引入。它实现了 Lock 接口,提供了比 synchronized 更灵活的锁操作。
核心类层次
Lock (interface)
└── ReentrantLock (class)
└── 内部维护 Sync (抽象类)
├── NonfairSync (非公平锁实现)
└── FairSync (公平锁实现)
基本用法
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockBasic {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 务必在 finally 中释放锁!
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
重要原则:使用
ReentrantLock时,必须在finally块中释放锁,否则锁无法被释放,可能导致死锁。这是与synchronized最大的不同——synchronized会自动释放锁。
可重入性验证
public class ReentrantVerify {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println("outer 获取锁成功");
System.out.println("当前持有锁的次数: " + lock.getHoldCount());
inner();
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
System.out.println("inner 获取锁成功(可重入)");
System.out.println("当前持有锁的次数: " + lock.getHoldCount());
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new ReentrantVerify().outer();
}
}
输出:
outer 获取锁成功
当前持有锁的次数: 1
inner 获取锁成功(可重入)
当前持有锁的次数: 2
可以看到,同一个线程调用 lock() 两次,锁的持有计数变为 2。释放时也需要调用两次 unlock(),计数归零后锁才真正被释放。
三、公平策略(Fairness Policy)
ReentrantLock 支持两种锁获取策略:公平锁和非公平锁。
公平锁 vs 非公平锁
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 等待队列顺序 | 严格 FIFO,先到先得 | 允许插队 |
| 吞吐量 | 较低 | 较高 |
| 线程饥饿风险 | 无 | 可能存在 |
| 适用场景 | 对公平性有要求 | 大多数业务场景 |
构造函数
// 默认非公平锁
ReentrantLock lock = new ReentrantLock();
// 指定公平策略
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
公平锁示例
public class FairnessDemo {
private final ReentrantLock lock;
public FairnessDemo(boolean fair) {
this.lock = new ReentrantLock(fair);
}
public void accessResource() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得了锁");
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
FairnessDemo demo = new FairnessDemo(true); // 公平锁
for (int i = 0; i < 5; i++) {
new Thread(demo::accessResource, "Thread-" + i).start();
}
}
}
公平锁输出(线程按启动顺序获取锁):
Thread-0 获得了锁
Thread-1 获得了锁
Thread-2 获得了锁
Thread-3 获得了锁
Thread-4 获得了锁
非公平锁输出(可能出现后启动的线程插队):
Thread-0 获得了锁
Thread-2 获得了锁 // 插队成功
Thread-1 获得了锁
Thread-4 获得了锁
Thread-3 获得了锁
实现原理简述
非公平锁在尝试获取锁时,会先通过 CAS 尝试一次"抢锁",如果成功就直接获取,无需进入等待队列。公平锁则严格检查等待队列中是否有前驱节点,如果有,即使锁空闲也排到队尾。
// 非公平锁的 tryAcquire 简化逻辑
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 直接 CAS 抢锁
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 可重入逻辑
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
// 公平锁的 tryAcquire 简化逻辑
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 检查是否有前驱等待者——关键区别
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
两者的核心差异就在 !hasQueuedPredecessors() 这个判断上。
四、Condition 条件变量
Condition 可以理解为 ReentrantLock 的"监视器等待/通知"机制,它类似于 synchronized 中的 wait()/notify()/notifyAll(),但更加强大。
与 synchronized 的对比
| 功能 | synchronized | ReentrantLock + Condition |
|---|---|---|
| 等待 | wait() |
condition.await() |
| 单发通知 | notify() |
condition.signal() |
| 全部通知 | notifyAll() |
condition.signalAll() |
| 超时等待 | 不支持 | await(long, TimeUnit) |
| 多个等待集 | 只有一个 | 可创建多个 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 condition = lock.newCondition();
private boolean ready = false;
// 等待线程
public void awaitSignal() throws InterruptedException {
lock.lock();
try {
while (!ready) {
System.out.println(Thread.currentThread().getName() + " 进入等待");
condition.await(); // 释放锁并等待
}
System.out.println(Thread.currentThread().getName() + " 被唤醒,继续执行");
} finally {
lock.unlock();
}
}
// 通知线程
public void doSignal() {
lock.lock();
try {
ready = true;
condition.signalAll(); // 唤醒所有等待线程
System.out.println("已发送通知");
} finally {
lock.unlock();
}
}
}
经典案例:有界阻塞队列
Condition 最经典的应用是有界阻塞队列(类似 ArrayBlockingQueue):
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBlockingQueue<T> {
private final Object[] items;
private int takeIndex, putIndex, count;
private final ReentrantLock lock;
private final Condition notEmpty; // 队列非空条件
private final Condition notFull; // 队列未满条件
public BoundedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
items = new Object[capacity];
lock = new ReentrantLock();
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
// 队列满时等待
while (count == items.length) {
notFull.await();
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
count++;
// 唤醒等待取元素的线程
notEmpty.signal();
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
// 队列空时等待
while (count == 0) {
notEmpty.await();
}
T item = (T) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
// 唤醒等待放元素的线程
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
这里有两个 Condition:
notEmpty:消费者线程等待队列非空notFull:生产者线程等待队列非满
使用多个 Condition 的好处是唤醒更加精确——生产者只唤醒消费者,消费者只唤醒生产者,避免了不必要的线程唤醒和竞争。
五、lockInterruptibly — 可中断的锁获取
lockInterruptibly() 允许线程在等待锁的过程中被其他线程中断并响应。
与 lock() 的区别
| 方法 | 响应中断 | 适用场景 |
|---|---|---|
lock() |
不响应 | 必须获取锁的场景 |
lockInterruptibly() |
响应 | 可取消的等待场景 |
示例
import java.util.concurrent.locks.ReentrantLock;
public class LockInterruptiblyDemo {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
try {
// 可中断地获取锁
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 获取锁成功");
// 模拟长时间工作
Thread.sleep(5000);
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断,放弃等待锁");
// 恢复中断状态
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
LockInterruptiblyDemo demo = new LockInterruptiblyDemo();
Thread t1 = new Thread(demo::performTask, "Thread-1");
Thread t2 = new Thread(demo::performTask, "Thread-2");
t1.start();
Thread.sleep(100); // 确保 t1 先获取锁
t2.start();
Thread.sleep(100); // t2 开始等待锁
// 中断 t2 的锁等待
t2.interrupt();
}
}
输出示例:
Thread-1 获取锁成功
Thread-2 被中断,放弃等待锁
Thread-1 释放锁
使用 lockInterruptibly() 时,如果一个线程正在等待锁,调用它的 interrupt() 方法会使其抛出 InterruptedException,从而优雅地退出等待。这在实现任务取消、超时控制等场景中非常有用。
使用建议
- 如果线程必须获取锁才能继续(否则逻辑无法进行),使用
lock() - 如果锁等待可以被取消(如用户取消了操作),使用
lockInterruptibly()
六、tryLock — 非阻塞尝试与超时获取
tryLock() 提供了非阻塞或带超时的锁获取方式,这是 synchronized 不具备的能力。
方法签名
// 非阻塞:立即返回,获取到返回 true,否则 false
boolean tryLock();
// 带超时:在时间内等待锁,可响应中断
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
非阻塞 tryLock 示例
import java.util.concurrent.locks.ReentrantLock;
public class TryLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void tryAccess() {
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 成功获取锁");
// 执行临界区代码
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 未能获取锁,执行备选路径");
// 执行其他逻辑,而不是阻塞等待
}
}
}
带超时的 tryLock
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockTimeoutDemo {
private final ReentrantLock lock = new ReentrantLock();
public boolean tryAcquireWithTimeout() {
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 在超时内获取了锁");
// 执行任务
return true;
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 超时未能获取锁");
return false;
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断");
Thread.currentThread().interrupt();
return false;
}
}
}
实际应用场景
// 防止死锁:使用 tryLock 尝试获取多个锁
public boolean transferMoney(Account from, Account to, int amount) {
long stopTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(2);
while (true) {
if (from.lock.tryLock()) {
try {
if (to.lock.tryLock()) {
try {
// 同时持有两个锁,执行转账
from.debit(amount);
to.credit(amount);
return true;
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
// 超时检查
if (System.nanoTime() >= stopTime) {
return false; // 无法获取所有锁,避免死锁
}
// 稍等片刻再重试
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 50));
}
}
七、ReentrantLock vs synchronized 全面对比
对比表格
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 语法简洁性 | ✅ 关键字,自动释放锁 | ❌ 需要显式 lock/unlock |
| 可重入性 | ✅ 支持 | ✅ 支持 |
| 公平性 | ❌ 非公平 | ✅ 可公平可非公平 |
| 中断响应 | ❌ 不响应(wait 时除外) | ✅ lockInterruptibly() |
| 超时获取 | ❌ 不支持 | ✅ tryLock(timeout) |
| 非阻塞获取 | ❌ 不支持 | ✅ tryLock() |
| 多个等待队列 | ❌ 只有一个 | ✅ 多个 Condition |
| 锁状态查询 | ❌ 难以查询 | ✅ getHoldCount(), isHeldByCurrentThread() 等 |
| 性能 (Java 8+) | ✅ 优化后接近 | ✅ 相当 |
| 错误风险 | ✅ 自动释放,不易出错 | ❌ 忘记 unlock 会死锁 |
性能对比
在 Java 6 之前,ReentrantLock 性能明显优于 synchronized。但自 Java 6 引入偏向锁、轻量级锁等优化后,两者性能已基本持平。现代 JVM 中,synchronized 在低竞争场景下可能更快(偏向锁),而高竞争场景下两者表现接近。
如何选择?
优先使用 synchronized,除非需要以下特性:
- 需要公平锁 —
synchronized不支持公平性 - 需要尝试获取锁 —
tryLock()用于非阻塞或超时获取 - 需要响应中断 —
lockInterruptibly()使等待可被取消 - 需要多个等待条件 — 多个
Condition实现更精细的线程协调 - 需要查询锁状态 —
getHoldCount()、hasQueuedThreads()等调试辅助
官方建议
Java 官方文档建议:在能够使用
synchronized的情况下,优先使用synchronized,因为它的使用更简单,不易出错,且 JVM 可以对其进行更好的优化(如锁消除、锁粗化等)。当synchronized无法满足需求时,再考虑使用ReentrantLock。
八、综合实战案例
案例:线程安全的银行账户
综合运用 ReentrantLock、Condition、tryLock 和可中断特性。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
private final Condition sufficientBalance = lock.newCondition();
private volatile double balance;
private final String accountNo;
public BankAccount(String accountNo, double initialBalance) {
this.accountNo = accountNo;
this.balance = initialBalance;
}
// 存款
public void deposit(double amount) {
lock.lock();
try {
if (amount <= 0) throw new IllegalArgumentException("存款金额必须为正数");
balance += amount;
System.out.printf("[%s] 存款 %.2f,余额:%.2f%n",
Thread.currentThread().getName(), amount, balance);
// 唤醒等待余额充足的所有线程
sufficientBalance.signalAll();
} finally {
lock.unlock();
}
}
// 取款(带超时等待)
public boolean withdraw(double amount, long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
lock.lockInterruptibly();
try {
// 循环等待余额充足
while (balance < amount) {
if (nanos <= 0) {
System.out.printf("[%s] 取款 %.2f 超时失败%n",
Thread.currentThread().getName(), amount);
return false;
}
System.out.printf("[%s] 余额不足,等待存款... 需取 %.2f,余额 %.2f%n",
Thread.currentThread().getName(), amount, balance);
nanos = sufficientBalance.awaitNanos(nanos);
}
balance -= amount;
System.out.printf("[%s] 取款 %.2f 成功,余额:%.2f%n",
Thread.currentThread().getName(), amount, balance);
return true;
} finally {
lock.unlock();
}
}
// 转账(使用 tryLock 防止死锁)
public static boolean transfer(BankAccount from, BankAccount to,
double amount, long timeout, TimeUnit unit)
throws InterruptedException {
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while (true) {
if (from.lock.tryLock()) {
try {
if (to.lock.tryLock()) {
try {
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
System.out.printf("转账 %.2f 成功: %s → %s%n",
amount, from.accountNo, to.accountNo);
return true;
} else {
System.out.printf("转账失败: %s 余额不足%n", from.accountNo);
return false;
}
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
if (System.nanoTime() >= stopTime) {
return false; // 超时
}
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 50));
}
}
public double getBalance() {
lock.lock();
try {
return balance;
} finally {
lock.unlock();
}
}
}
测试代码
import java.util.concurrent.*;
public class BankAccountTest {
public static void main(String[] args) throws Exception {
BankAccount alice = new BankAccount("Alice", 1000);
BankAccount bob = new BankAccount("Bob", 500);
// 1. 取款测试(带超时等待)
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
try {
boolean success = alice.withdraw(2000, 3, TimeUnit.SECONDS);
System.out.println("取款结果: " + success);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 等待一会让取款线程先进入等待
Thread.sleep(500);
// 存款,让取款线程有机会完成
executor.submit(() -> {
alice.deposit(1500);
});
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
// 2. 转账测试
boolean transferResult = BankAccount.transfer(alice, bob, 800, 2, TimeUnit.SECONDS);
System.out.println("转账结果: " + transferResult);
System.out.println("Alice 余额: " + alice.getBalance());
System.out.println("Bob 余额: " + bob.getBalance());
}
}
九、常见问题与最佳实践
常见错误
1. 忘记在 finally 中释放锁
// ❌ 错误写法
public void badMethod() {
lock.lock();
// 如果这里抛出异常,锁永远不会释放!
doSomething();
lock.unlock();
}
// ✅ 正确写法
public void goodMethod() {
lock.lock();
try {
doSomething();
} finally {
lock.unlock();
}
}
2. 在持有锁时调用外部方法
// ❌ 可能导致死锁或性能问题
public void process() {
lock.lock();
try {
// 调用外部未知方法——可能也尝试获取锁
externalService.doSomething();
} finally {
lock.unlock();
}
}
3. Condition 使用不当
// ❌ 在条件等待中使用 if 而不是 while
if (!condition) {
condition.await(); // 可能被虚假唤醒
}
// ✅ 始终在循环中等待
while (!condition) {
condition.await();
}
最佳实践总结
- 始终在
finally中释放锁,确保异常时也不会锁泄露 - 尽量缩小锁的作用域,只保护必要的代码段
- 使用
while循环包裹Condition.await(),防止虚假唤醒 - 优先选择
synchronized,除非确实需要ReentrantLock的特定功能 - 避免在持有锁时调用未知的外部方法(可能导致死锁)
- 考虑使用
tryLock()获取多个锁,避免死锁 - 理解公平锁的性能代价,默认使用非公平锁即可
- 使用
lockInterruptibly()实现可取消的等待
十、总结
ReentrantLock 是 Java 并发编程中的一个重要工具,它提供了远超 synchronized 的灵活性:
| 核心能力 | 解决的问题 |
|---|---|
| 可重入 | 同一线程可多次获取锁,避免递归/嵌套调用时的死锁 |
| 公平策略 | 按线程等待时间分配锁,避免线程饥饿 |
| Condition | 多个等待条件,实现更精细的线程协调 |
| lockInterruptibly | 响应中断的锁获取,实现可取消的操作 |
| tryLock | 非阻塞/超时获取锁,避免死锁 |
理解并正确使用 ReentrantLock,是 Java 并发编程进阶的必经之路。但也要记住:能用 synchronized 解决的问题,尽量用 synchronized。当 synchronized 无法满足需求时,ReentrantLock 就是你的得力武器。
参考:Java Concurrency in Practice、Java 官方文档、java.util.concurrent.locks 包源码