问题
什么是可重入锁,怎么实现可重入锁?
答案
1. 核心概念
可重入锁(Reentrant Lock):也叫递归锁,指的是同一个线程在持有锁的情况下,可以再次获取该锁,而不会造成死锁。
通俗理解:
- 一个线程获得锁后,可以多次进入被该锁保护的代码块
- 每次进入锁的计数器加 1,每次退出锁的计数器减 1
- 当计数器为 0 时,锁才真正释放
2. 为什么需要可重入锁
2.1 避免死锁
如果锁不可重入,同一个线程多次获取锁会导致死锁:
// 假设锁不可重入
synchronized (this) {
System.out.println("第一次获取锁");
synchronized (this) { // 死锁!同一个线程无法再次获取锁
System.out.println("第二次获取锁");
}
}
2.2 支持递归调用
public synchronized void methodA() {
System.out.println("methodA");
methodB(); // 调用另一个同步方法
}
public synchronized void methodB() {
System.out.println("methodB");
// 如果锁不可重入,这里会死锁
}
2.3 简化代码结构
可重入锁让方法调用链中的每个方法都可以独立使用 synchronized,无需担心死锁。
3. Java 中的可重入锁
Java 中的以下锁都是可重入的:
- synchronized
- ReentrantLock
- ReentrantReadWriteLock
4. synchronized 的可重入实现
4.1 实现原理
synchronized 通过对象头的 Mark Word 和锁计数器实现可重入:
// 对象头结构(Mark Word,64 位 JVM)
| 锁状态 | 存储内容 | 锁标志位 |
|---------|----------------------------|---------|
| 偏向锁 | 线程 ID + Epoch + 对象分代年龄 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向 Monitor 对象的指针 | 10 |
重入逻辑:
- 线程首次获取锁:在对象头记录线程 ID,计数器设为 1
- 线程再次获取锁:检查线程 ID 匹配,计数器 +1
- 线程释放锁:计数器 -1,当计数器为 0 时真正释放锁
4.2 代码示例
public class SynchronizedReentrant {
public synchronized void outer() {
System.out.println("外层方法");
inner(); // 调用内层方法
}
public synchronized void inner() {
System.out.println("内层方法");
}
public static void main(String[] args) {
SynchronizedReentrant obj = new SynchronizedReentrant();
obj.outer(); // 正常执行,不会死锁
}
}
// 输出:
// 外层方法
// 内层方法
执行流程:
- 线程调用
outer(),获取obj的锁,计数器 = 1 - 线程调用
inner(),检测到已持有锁,计数器 = 2 inner()执行完,计数器 = 1outer()执行完,计数器 = 0,释放锁
5. ReentrantLock 的可重入实现
5.1 实现原理
ReentrantLock 基于 AQS 的 state 变量和线程持有者实现可重入:
public class ReentrantLock implements Lock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// 记录锁的持有线程
private transient Thread exclusiveOwnerThread;
// 尝试获取锁(非公平)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取 AQS 的 state
if (c == 0) { // 锁未被占用
if (compareAndSetState(0, acquires)) { // CAS 获取锁
setExclusiveOwnerThread(current); // 记录持有线程
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 可重入逻辑
int nextc = c + acquires; // 计数器 +1
if (nextc < 0) // 溢出检查
throw new Error("Maximum lock count exceeded");
setState(nextc); // 更新 state
return true;
}
return false;
}
// 尝试释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 计数器 -1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 计数器为 0,真正释放锁
free = true;
setExclusiveOwnerThread(null); // 清除持有线程
}
setState(c); // 更新 state
return free;
}
}
}
核心要素:
- state 变量:表示锁的持有次数(0 表示未锁定,>0 表示重入次数)
- exclusiveOwnerThread:记录当前持有锁的线程
- 重入判断:检查当前线程是否等于
exclusiveOwnerThread
5.2 代码示例
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock(); // 第一次获取锁,state = 1
try {
System.out.println("外层方法,state = " + lock.getHoldCount());
inner(); // 调用内层方法
} finally {
lock.unlock(); // 释放锁,state = 0
}
}
public void inner() {
lock.lock(); // 第二次获取锁(可重入),state = 2
try {
System.out.println("内层方法,state = " + lock.getHoldCount());
} finally {
lock.unlock(); // 释放锁,state = 1
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
example.outer();
}
}
// 输出:
// 外层方法,state = 1
// 内层方法,state = 2
6. 手动实现一个简单的可重入锁
public class SimpleReentrantLock {
// 记录持有锁的线程
private Thread owner;
// 重入计数器
private int count = 0;
// 加锁
public synchronized void lock() {
Thread current = Thread.currentThread();
// 如果是当前线程持有锁,计数器 +1(可重入)
if (owner == current) {
count++;
return;
}
// 否则等待锁释放
while (owner != null) {
try {
wait(); // 阻塞等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 获取锁
owner = current;
count = 1;
}
// 解锁
public synchronized void unlock() {
if (Thread.currentThread() != owner) {
throw new IllegalMonitorStateException("当前线程未持有锁");
}
count--; // 计数器 -1
if (count == 0) { // 真正释放锁
owner = null;
notify(); // 唤醒等待的线程
}
}
}
测试代码:
public class TestSimpleReentrantLock {
private static SimpleReentrantLock lock = new SimpleReentrantLock();
public static void outer() {
lock.lock();
try {
System.out.println("外层方法");
inner();
} finally {
lock.unlock();
}
}
public static void inner() {
lock.lock(); // 可重入
try {
System.out.println("内层方法");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
outer(); // 正常执行,不会死锁
}
}
7. 实现要点总结
实现可重入锁需要:
- 记录持有锁的线程(
Thread owner或对象头的线程 ID) - 维护重入计数器(
int count或 AQS 的state) - 加锁逻辑:
- 如果锁未被持有,获取锁并设置计数器为 1
- 如果当前线程已持有锁,计数器 +1(可重入)
- 否则阻塞等待
- 解锁逻辑:
- 计数器 -1
- 当计数器为 0 时,清除持有线程,真正释放锁
8. 对比:可重入 vs 不可重入
| 特性 | 可重入锁 | 不可重入锁 |
|---|---|---|
| 同一线程多次获取 | ✅ 允许 | ❌ 死锁 |
| 递归调用 | ✅ 支持 | ❌ 不支持 |
| 实现复杂度 | 需要计数器和线程记录 | 简单(只需一个标志位) |
| 典型代表 | synchronized, ReentrantLock | 无(Java 标准库中的锁都是可重入的) |
9. 性能考量
- 计数器开销:每次重入需要增减计数器,但开销很小
- 线程检查开销:需要比较当前线程与持有线程,开销也很小
- 避免死锁收益:可重入锁极大简化了代码结构,避免了复杂的死锁场景
10. 注意事项
10.1 加锁和解锁次数必须匹配
lock.lock();
lock.lock(); // 重入一次
try {
// 业务代码
} finally {
lock.unlock();
lock.unlock(); // 必须解锁两次
}
10.2 不要忘记释放锁
lock.lock();
try {
// 业务代码
} finally {
lock.unlock(); // 必须在 finally 中释放
}
11. 总结
可重入锁的核心:
- 定义:同一个线程可以多次获取同一把锁,不会死锁
- 作用:支持递归调用,简化代码结构
- 实现要素:
- 记录持有锁的线程(Thread ID 或 exclusiveOwnerThread)
- 维护重入计数器(对象头的计数或 AQS 的 state)
- Java 中的可重入锁:synchronized、ReentrantLock、ReentrantReadWriteLock
面试要点:
- 明确可重入锁的定义和作用
- 说明 synchronized 和 ReentrantLock 的可重入实现原理
- 强调计数器和线程持有者两个关键要素
- 能够手写一个简单的可重入锁(加分项)
记忆口诀:
- 线程记录要保存,计数器来控状态
- 加锁加一减锁减,归零释放唤后来