【多线程】可重入锁 Reentrant Lock
【多线程】可重入锁 Reentrant Lock
一、核心定义
可重入锁,顾名思义,是指一个线程在已经持有某个锁的情况下,可以再次成功获取该锁而不会被阻塞。
换句话说,同一个线程可以多次进入被同一把锁保护的代码块。
二、一个生动的比喻
想象一个房间(共享资源)有一把门锁(锁)。
- 非可重入锁:你(线程)拿着钥匙进入房间后,把门锁上了。如果你在房间里又想开门(再次获取锁),你会发现门从里面也打不开了(线程被阻塞,导致死锁)。这显然不合理。
- 可重入锁:你拿着钥匙进入房间后,把门锁上了。这个门锁很智能,它认识主人。当你在房间里再次尝试开门时,锁识别出“哦,原来是主人你啊”,就直接让你通过了,并且会在内部记录你进入的次数(重入次数)。你离开房间时,需要出来同样次数,门才会真正锁上。
三、为什么需要可重入锁?
最常见的场景是在递归调用和多个方法相互调用的情况下。
让我们看一个非可重入锁会导致问题的例子:
// 假设我们有一个非可重入锁
public class NonReentrantLock {private boolean isLocked = false;public synchronized void lock() throws InterruptedException {while (isLocked) {wait();}isLocked = true;}public synchronized void unlock() {isLocked = false;notify();}
}// 使用这个锁的类
public class Counter {private NonReentrantLock lock = new NonReentrantLock();private int count = 0;public void increment() {try {lock.lock(); // 第一次获取锁count++;doSomethingElse(); // 调用另一个也需要锁的方法} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public void doSomethingElse() {try {lock.lock(); // 第二次尝试获取同一个锁// ... 做一些操作} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}
}
问题分析:
- 线程A调用
increment()
,成功获取锁 (isLocked
变为true
)。 - 在
increment()
内部,调用了doSomethingElse()
。 doSomethingElse()
也试图获取同一把锁。- 由于这是一个非可重入锁,它检查到
isLocked
为true
(即使是被自己锁住的),于是线程A在lock.lock()
这里等待自己释放锁。 - 这就导致了经典的 自死锁 情况。线程A永远卡在那里,程序无法继续执行。
四、可重入锁是如何实现的?
可重入锁通常通过以下两个属性来实现:
- 持有者线程:记录当前是哪个线程持有此锁。
- 重入计数器:记录同一个线程获取该锁的次数。
其工作流程如下:
- 当线程第一次请求锁时,计数器从0变为1,并记录持有者线程。
- 当同一个线程再次请求锁时,计数器简单地递增(比如从1变为2)。
- 当线程释放锁时,计数器递减。
- 只有当计数器减回到0时,锁才会被真正释放,其他线程才能获取。
五、在Java中的实现
在Java中,最常用的两种可重入锁是:
-
synchronized
关键字
Java内置的synchronized
关键字实现的锁就是可重入的。这是最常用的方式。public class ReentrantExample {public synchronized void methodA() {System.out.println("In methodA");methodB(); // 调用另一个synchronized方法,不会阻塞!}public synchronized void methodB() {System.out.println("In methodB");} }
上面的代码可以完美运行,因为
synchronized
是可重入的。 -
java.util.concurrent.locks.ReentrantLock
类
这是Java并发包(java.util.concurrent
)中提供的一个显式的可重入锁实现。它比synchronized
更灵活,提供了更多功能,如尝试获取锁、可中断的锁获取、公平锁等。import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock(); // 获取锁try {count++;doSomethingElse(); // 重入} finally {lock.unlock(); // 必须在finally块中释放锁}}public void doSomethingElse() {lock.lock(); // 同一个线程,再次获取同一把锁try {// ... 做一些操作System.out.println("重入成功,当前重入次数: " + lock.getHoldCount());} finally {lock.unlock();}} }
六、可重入锁的优点
- 避免自死锁:解决了递归和方法调用链中的锁问题。
- 逻辑更自然:符合程序员对锁行为的直觉,同一个线程对自己的锁应该有“特权”。
七、总结
特性 | 可重入锁 | 非可重入锁 |
---|---|---|
核心特性 | 同一线程可多次获取 | 同一线程只能获取一次 |
递归调用 | 支持 | 导致死锁 |
方法互调 | 支持 | 导致死锁 |
实现复杂度 | 稍高(需记录线程和计数器) | 简单 |
常见例子 | Java synchronized , ReentrantLock | 简单的自旋锁(未实现重入) |
简单来说,可重入锁是现代多线程编程的标配,它极大地简化了在面向对象编程中复杂的调用关系下的同步代码编写。你在Java中遇到的大多数锁默认都是可重入的。