锁重入(简单理解)

什么是锁重入?

一个线程拿到锁之后,不用释放,还能再拿同一把锁。

举个例子

你进家门(拿到家门锁),进了客厅后,想进卧室(卧室也用同一把钥匙),不用先把家门钥匙还回去,直接用手里的钥匙开门就行。

Java 里的情况

  1. synchronized 关键字

    比如一个同步方法 A 里调用了另一个同步方法 B,且两者用的是同一把锁(比如都是同一个对象的锁),线程进了 A 之后,能直接进 B,不用等自己释放 A 的锁。

  2. ReentrantLock 锁

    手动加锁时,线程第一次lock()拿到锁后,还能再lock()一次(重入),但要记得解锁同样多次(unlock()),最后一次解锁后,其他线程才能抢。

为什么需要它?

避免死锁。如果不支持重入,线程进了 A 之后想进 B,就得先放 A 的锁,可自己还拿着 A 的锁呢,就会卡住(死锁)。

关键点

  • 必须是「同一把锁」才能重入,不同的锁不行。
  • ReentrantLock 要记得加几次锁就解几次,不然锁会一直被占着。

以下是几个直观的锁重入代码示例,分别展示 synchronized 和 ReentrantLock 的重入特性:

示例 1:synchronized 方法的重入(同一对象锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SyncReentrantDemo {
public static void main(String[] args) {
// 线程调用test1(),会触发嵌套的同步方法调用
new Thread(new Task()).start();
}

static class Task implements Runnable {
@Override
public void run() {
test1(); // 调用同步方法test1
}

// 同步方法1(锁是当前Task对象)
private synchronized void test1() {
System.out.println(Thread.currentThread().getName() + " 进入test1");
test2(); // 调用同步方法test2(同一把锁)
System.out.println(Thread.currentThread().getName() + " 离开test1");
}

// 同步方法2(锁也是当前Task对象)
private synchronized void test2() {
System.out.println(Thread.currentThread().getName() + " 进入test2");
test3(); // 继续调用同步方法test3(同一把锁)
System.out.println(Thread.currentThread().getName() + " 离开test2");
}

// 同步方法3(同一把锁)
private synchronized void test3() {
System.out.println(Thread.currentThread().getName() + " 进入test3");
System.out.println(Thread.currentThread().getName() + " 离开test3");
}
}
}

运行结果(同一线程连续进入 3 个同步方法,体现重入):

1
2
3
4
5
6
Thread-0 进入test1
Thread-0 进入test2
Thread-0 进入test3
Thread-0 离开test3
Thread-0 离开test2
Thread-0 离开test1

示例 2:synchronized 代码块的重入(同一锁对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SyncBlockReentrantDemo {
// 自定义锁对象
private static final Object lock = new Object();

public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) { // 第一次获取lock锁
System.out.println("外层代码块:拿到锁");
synchronized (lock) { // 再次获取同一把lock锁(重入)
System.out.println("内层代码块:再次拿到锁");
}
System.out.println("外层代码块:释放锁");
}
}).start();
}
}

运行结果(同一线程在嵌套代码块中重复获取锁):

1
2
3
外层代码块:拿到锁
内层代码块:再次拿到锁
外层代码块:释放锁

示例 3:ReentrantLock 的重入(显式锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
private static final ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
new Thread(() -> {
lock.lock(); // 第一次加锁
try {
System.out.println("第一次加锁,计数器:" + lock.getHoldCount());
method1(); // 调用方法再次加锁
} finally {
lock.unlock(); // 最后一次解锁
System.out.println("最终解锁后,计数器:" + lock.getHoldCount());
}
}).start();
}

private static void method1() {
lock.lock(); // 第二次加锁(重入)
try {
System.out.println("第二次加锁,计数器:" + lock.getHoldCount());
method2(); // 继续加锁
} finally {
lock.unlock(); // 第二次解锁
System.out.println("第二次解锁后,计数器:" + lock.getHoldCount());
}
}

private static void method2() {
lock.lock(); // 第三次加锁(重入)
try {
System.out.println("第三次加锁,计数器:" + lock.getHoldCount());
} finally {
lock.unlock(); // 第三次解锁
System.out.println("第三次解锁后,计数器:" + lock.getHoldCount());
}
}
}

运行结果(通过 getHoldCount() 查看锁计数器变化,体现重入次数):

1
2
3
4
5
6
第一次加锁,计数器:1
第二次加锁,计数器:2
第三次加锁,计数器:3
第三次解锁后,计数器:2
第二次解锁后,计数器:1
最终解锁后,计数器:0

总结

  • 锁重入的核心是「同一线程重复获取同一把锁」,无需释放后再获取。
  • synchronized 自动维护重入计数器,ReentrantLock 需手动保证 lock() 和 unlock() 次数一致(否则锁无法释放)。
  • 嵌套调用同步方法 / 代码块时,重入特性避免了线程自己等自己的死锁问题。