并发编程-ReentranLock
特点
可重入特点
在下面我分析的加锁流程就可以看到
加锁后可中断
实例
注意这里不是调用lock方法加锁
package org.example.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;/*** @author 2405993739* @description: 演示ReentranLock* @date 2025/6/13 14:37*/
@Slf4j
public class ReentranLockDemo {public static void main(String[] args) {interruptTest();}// 可中断static void interruptTest() {ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {try {lock.lockInterruptibly();log.debug("t1 start");Thread.sleep(2000);log.debug("t1 end");} catch (InterruptedException e) {log.debug("t1 中断");e.printStackTrace();} finally {lock.unlock();}}, "t1");Thread t2 = new Thread(() -> {try {log.debug("t2 start");t1.interrupt(); // 中断t1线程log.debug("t2 end");} finally {// lock.unlock();}}, "t2");t1.start();t2.start();}
}
在这个示例中,t1
线程调用lockInterruptibly()
方法等待锁,而t2
线程在启动后立即中断t1
线程。当t1
线程被中断时,会抛出InterruptedException
异常,并打印出“t1 interrupted”。
底层实现-基于JDK8
下面结合 JDK 8 的源码,重点剖析 ReentrantLock
的 可打断获取 特性(lockInterruptibly()
)是如何实现的。
1. 方法签名与调用链
ReentrantLock
在 API 层提供了:
public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);
}
-
调用的是内部
Sync
(继承自 AQS)的acquireInterruptibly(int)
方法; -
Sync
有两种实现——公平和非公平,它们都继承自AbstractQueuedSynchronizer
。
2. AQS.acquireInterruptibly 源码(简化)
public final void acquireInterruptibly(int arg) throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (!tryAcquire(arg))doAcquireInterruptibly(arg);
}
-
先检查并清除当前线程中断状态:若已中断,则立刻抛
InterruptedException
; -
调用子类的
tryAcquire(arg)
(即Sync.tryAcquire
)尝试非阻塞式获取锁; -
若未获取到,则进入可打断的阻塞获取
doAcquireInterruptibly(arg)
。
3. doAcquireInterruptibly 逻辑(关键片段)
private void doAcquireInterruptibly(int arg) throws InterruptedException {final Node node = addWaiter(Node.EXCLUSIVE);int failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return;}// 阻塞前检查中断if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}
-
排队
-
addWaiter(Node.EXCLUSIVE)
:将当前线程封装成Node
,加入 AQS 的同步队列尾部。
-
-
自旋+挂起
-
循环判断:只有当自己是队列头的下一个节点(
p == head
)并且能tryAcquire
成功,才出队并返回; -
否则,通过
parkAndCheckInterrupt()
挂起自己。
-
-
中断检测
-
parkAndCheckInterrupt()
返回true
表示线程在挂起期间被中断,此时 立刻 抛出InterruptedException
,并在finally
中进行当前节点的清理(cancelAcquire
),退出获取流程。
-
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}
这里的interrupted除了返回当前中断状态,还可以清除中断状态,让下一次调用park方法生效
4. 整体流程小结
-
调用
lockInterruptibly()
→acquireInterruptibly(1)
-
快速路径:
tryAcquire(1)
成功则立即返回; -
阻塞路径:
doAcquireInterruptibly(1)
-
排入同步队列
-
循环尝试唤醒(
head
节点出队后依次轮到自己) -
每次挂起前后都检查中断 —— 一旦被中断,立刻抛
InterruptedException
并清理自身排队节点
-
-
退出
-
成功获取:
setHead(node)
,锁状态已由tryAcquire
更新 -
被中断:在
finally
清理后退出。
-
公平锁
底层实现-非公平锁加锁
我们来看看非公平锁的加锁代码:
final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}
尝试获取锁:
-
首先,通过
compareAndSetState(0, 1)
方法尝试将锁的状态(state)从 0 设置为 1。这个操作是原子性的,基于硬件指令集实现,确保在同一时间只有一个线程能够成功地将状态从 0 改为 1。这里面是没有关于等待队列的任何判断的,所以是非公平 -
如果
compareAndSetState(0, 1)
返回true
,表示当前线程成功获取了锁,此时会调用setExclusiveOwnerThread(Thread.currentThread())
方法,将当前线程设置为独占锁的拥有者。
处理获取锁失败的情况:
-
如果
compareAndSetState(0, 1)
返回false
,表示锁已经被其他线程持有,当前线程未能成功获取锁。 -
在这种情况下,会调用
acquire(1)
方法。acquire(1)
是AbstractQueuedSynchronizer
类中的方法,它会尝试再次获取锁,并在获取失败时将当前线程加入到等待队列中,直到锁被释放并成功获取。我们来看看这里面公不公平
acquire(1):
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}
尝试直接获取锁:
-
首先调用
tryAcquire(arg)
方法尝试直接获取锁。这个方法需要子类实现,具体实现会根据不同的锁类型(如ReentrantLock
的公平锁和非公平锁)有所不同。如果当前线程成功获取到锁,tryAcquire(arg)
返回true
,则acquire
方法直接返回,不再执行后续操作。 -
如果
tryAcquire(arg)
返回false
,说明当前线程没有成功获取到锁,需要进入等待队列。
将线程加入等待队列:
-
调用
addWaiter(Node.EXCLUSIVE)
方法将当前线程封装成一个独占模式的节点(Node.EXCLUSIVE
),并将其添加到等待队列的尾部。这个方法会返回新创建的节点。 -
addWaiter
方法内部使用了 CAS 操作来确保线程安全地将节点添加到队列中。
阻塞等待获取锁:
-
调用
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法,该方法会让当前线程在等待队列中阻塞,直到成功获取到锁为止。 -
acquireQueued
方法内部是一个死循环,不断尝试获取锁。如果当前节点的前驱节点是头节点,则尝试获取锁;如果获取成功,则将当前节点设置为新的头节点,并返回中断状态。 -
如果当前节点的前驱节点不是头节点,或者获取锁失败,则当前线程会被阻塞(通过
LockSupport.park(this)
实现),直到被其他线程唤醒。
自我中断:
-
如果在等待过程中线程被中断过,
acquireQueued
方法会返回true
。此时,acquire
方法会调用selfInterrupt()
方法进行自我中断,即将当前线程的中断状态重新设置为true
。 -
这是因为在等待过程中,线程可能会被多次唤醒并重新尝试获取锁,每次唤醒后中断状态都会被重置。因此,需要在最后统一处理中断状态。
这里面还是没有关于公平的体现。我们再进入acquireQueued来看看是否公平
tryAcquire:
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
可以看到,他获取了线程信息后,取得线程加锁数的状态,如果没有加锁,锁计数器为0,直接调用compareAndSetState来加锁。中间并没有任何公平的判断。
底层实现-非公平锁加锁
再来看看公平锁的加锁代码:
上面的调用链和非公平的差不多,我们直接看具体加锁实现的方法-tryAcquire:
hasQueuedPredecessors
可以看到,当他准备调用compareAndSetState加锁前,会调用一个hasQueuedPredecessors的方法,
hasQueuedPredecessors这个方法:
hasQueuedPredecessors()
方法用于判断在当前线程之前,是否已经存在排队等待的线程。该方法在公平锁的实现中尤为重要,因为它决定了当前线程是否应该立即尝试获取锁,还是应该加入等待队列。
返回 true
:表示队列中存在等待线程,当前线程需要加入等待队列。
返回 false
:表示队列中没有等待线程,或者当前线程是队列中的第一个等待线程,可以尝试获取锁。
这就是公平锁的体现,他会在加锁前判断是否能加锁!!!主打一个先来后到!!!
尝试直接获取锁:
-
首先调用
tryAcquire(arg)
方法尝试直接获取锁。这个方法需要子类实现,具体实现会根据不同的锁类型(如ReentrantLock
的公平锁和非公平锁)有所不同。如果当前线程成功获取到锁,tryAcquire(arg)
返回true
,则acquire
方法直接返回,不再执行后续操作。 -
如果
tryAcquire(arg)
返回false
,说明当前线程没有成功获取到锁,需要进入等待队列。
将线程加入等待队列:
-
调用
addWaiter(Node.EXCLUSIVE)
方法将当前线程封装成一个独占模式的节点(Node.EXCLUSIVE
),并将其添加到等待队列的尾部。这个方法会返回新创建的节点。 -
addWaiter
方法内部使用了 CAS 操作来确保线程安全地将节点添加到队列中。
阻塞等待获取锁:
-
调用
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法,该方法会让当前线程在等待队列中阻塞,直到成功获取到锁为止。 -
acquireQueued
方法内部是一个死循环,不断尝试获取锁。如果当前节点的前驱节点是头节点,则尝试获取锁;如果获取成功,则将当前节点设置为新的头节点,并返回中断状态。 -
如果当前节点的前驱节点不是头节点,或者获取锁失败,则当前线程会被阻塞(通过
LockSupport.park(this)
实现),直到被其他线程唤醒。
自我中断:
-
如果在等待过程中线程被中断过,
acquireQueued
方法会返回true
。此时,acquire
方法会调用selfInterrupt()
方法进行自我中断,即将当前线程的中断状态重新设置为true
。 -
这是因为在等待过程中,线程可能会被多次唤醒并重新尝试获取锁,每次唤醒后中断状态都会被重置。因此,需要在最后统一处理中断状态。
条件变量
实例
如果没有自定义条件变量,ReentrantLock将无法提供等待和唤醒的功能!!!
ReentranLock它本身并不直接提供条件变量的功能,
例如,在以下代码中,如果没有condition.await()
和condition.signal()
,线程将无法正确地等待和唤醒:
lock.lock();
try {while (!conditionIsTrue) {condition.await(); // 等待条件成立}// 使用共享资源
} finally {lock.unlock();
}
ReentrantLock的实现基于AbstractQueuedSynchronizer(AQS),而AQS内部支持多个条件变量。每个Condition
对象实际上是一个ConditionObject
,它是AQS的内部类,用于管理等待队列和唤醒操作。因此,虽然ReentrantLock本身不直接拥有条件变量,但它通过AQS的机制支持了多个条件变量的创建和使用
与ReentrantLock不同,synchronized
关键字是隐式地与锁对象绑定的,其内部的等待队列(WaitSet)是自动管理的。
而ReentrantLock则需要显式地创建条件变量,并且可以支持多个条件变量
实例
public static void mutiConditionTest(int times) {ReentrantLock lock = new ReentrantLock();Condition conditionA = lock.newCondition();Condition conditionB = lock.newCondition();final boolean[] turnA = {true}; // 使用数组包装以便内部线程修改Thread threadA = new Thread(() -> {for (int i = 0; i < times; i++) {lock.lock();try {while (!turnA[0]) {conditionA.await();}log.debug("A");turnA[0] = false;conditionB.signal();} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}}, "Thread-A");Thread threadB = new Thread(() -> {for (int i = 0; i < times; i++) {lock.lock();try {while (turnA[0]) {conditionB.await();}log.debug("B");turnA[0] = true;conditionA.signal();} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}}, "Thread-B");threadA.start();threadB.start();}
✅ 初始化阶段:
-
turnA[0] = true
(A 线程可以先打印); -
所以第一次 A 执行时不会进入
await()
,直接打印。
❌ 第二次循环(A):
-
A 执行完一次后设置:
turnA[0] = false
; -
B 被唤醒打印一次,接着设置
turnA[0] = true
并唤醒 A; -
所以第二次 A 被唤醒后能直接执行,也不会等待。
⛔ 那什么时候会真的进入 conditionA.await()
呢?
如果 A 提前获取了锁,但 B 尚未改变 turnA 为 true,那 A 会在判断:
while (!turnA[0]) { conditionA.await(); // ❗会等待,因为现在还轮不到它执行 }
🔁 这种情况通常发生在高并发或不同线程抢锁速度不同的场景中。
🔄 模拟一个可能触发 await 的时序:
-
A 执行完一次,turnA = false,唤醒 B;
-
B 还没来得及执行,A 抢到了锁(因为操作系统调度是随机的);
-
A 进入下一轮,判断
!turnA[0] == true
; -
A 被迫执行
conditionA.await()
,直到 B 执行完并signal()
唤醒 A。
底层实现-基于JDK8
底层基本实现-JDK17版本
接口继承图
ReentrantLock 是 Java 并发包(JUC)中提供的一种可重入锁,它实现了 Lock
接口,并通过 AbstractQueuedSynchronizer
(AQS)来实现其底层同步机制。ReentrantLock 支持公平锁和非公平锁两种模式,其中默认使用非公平锁。下面我们将结合你提供的类图和证据材料,详细分析 ReentrantLock 的实现原理,特别是非公平锁的加锁流程。
ReentrantLock 的类结构
ReentrantLock 实现了 Lock
接口,其内部维护了一个 Sync
同步器,该同步器继承自 AQS。
Sync
类有两个子类:FairSync
(公平锁)和 NonfairSync
(非公平锁)。
根据构造函数的参数,ReentrantLock 可以选择使用公平锁或非公平锁。
默认情况下,构造函数会创建一个非公平锁:
如果需要显式指定公平锁,则使用带参数的构造函数:
非公平锁的加锁流程
1. lock 方法的调用链
当调用 ReentrantLock.lock()
方法时,实际上会调用内部 Sync
对象的 lock()
方法。由于 Sync
是一个抽象类,具体的实现由 NonfairSync
提供。因此,非公平锁的加锁流程如下:
调用Sync
对象的 lock()
跟进去
继续跟
由于是抽象,我们找他的实现类,我们先找非公平的,在 NonfairSync
中,lock()
方法的实现如下:
2. CAS 操作与加锁成功
加锁解析:
// 定义一个最终方法 initialTryLock,表示该方法不能被子类重写
final boolean initialTryLock() {// 获取当前线程的引用Thread current = Thread.currentThread();
// 尝试将锁的状态从0(未锁定)更新为1(锁定)// 这是一个原子操作,确保线程安全if (compareAndSetState(0, 1)) { // first attempt is unguarded// 如果成功更新状态,设置当前线程为锁的持有者setExclusiveOwnerThread(current);// 返回 true,表示成功获取锁return true;}// 如果锁已经被当前线程持有,则允许重入else if (getExclusiveOwnerThread() == current) {// 获取当前锁的状态并加1,表示重入次数增加int c = getState() + 1;// 检查加1后是否发生溢出if (c < 0) // overflow// 如果发生溢出,抛出错误throw new Error("Maximum lock count exceeded");// 更新锁的状态setState(c);// 返回 true,表示成功获取锁return true;}// 如果以上条件都不满足,表示当前线程无法获取锁else// 返回 false,表示获取锁失败return false;
}
-
获取当前线程:首先获取当前线程的引用。
-
尝试获取锁:
-
使用
compareAndSetState(0, 1)
尝试将锁的状态从0更新为1。 -
如果成功,设置当前线程为锁的持有者,并返回
true
。
-
3. 可重入与加锁失败(可重入特点)
-
检查是否已经持有锁:
-
如果锁已经被当前线程持有,允许重入。
-
获取当前锁的状态并加1,检查是否发生溢出。
-
如果没有溢出,更新锁的状态,并返回
true
。
-
-
返回失败:
-
如果以上条件都不满足,表示当前线程无法获取锁,返回
false
。
-
这段代码确保了锁的获取过程是线程安全的,并且支持锁的重入。
解锁流程
1 调用AQS的释放锁方法
2
AQS的释放锁 release
方法
public final boolean release(int arg) {if (tryRelease(arg)) {signalNext(head);return true;}return false;}
参数:arg
是释放操作的参数,通常表示锁的持有次数。
逻辑:
-
调用
tryRelease(arg)
尝试释放锁。 -
如果
tryRelease
返回true
,表示锁成功释放。 -
返回
true
表示释放成功,否则返回false
。
3 尝试解锁 tryRelease
方法
protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = (c == 0);if (free)setExclusiveOwnerThread(null);setState(c);return free;
}
参数:releases
表示要释放的锁的持有次数。
逻辑:
-
计算新的状态值
c
,即当前状态减去释放次数。 -
检查当前线程是否是锁的持有者,如果不是则抛出
IllegalMonitorStateException
异常。 -
如果新的状态值
c
为0,表示锁完全释放,设置锁的持有线程为null
。 -
更新状态值为
c
。 -
返回
free
,即锁是否完全释放。
4 解锁成功后 唤醒其他线程signalNext
方法
private static void signalNext(Node h) {Node s;if (h != null && (s = h.next) != null && s.status != 0) {s.getAndUnsetStatus(WAITING);LockSupport.unpark(s.waiter);}
}
参数:h
是等待队列的头节点。
逻辑:
-
检查头节点是否为空,以及头节点的下一个节点是否不为空且其等待状态不为0。
-
如果条件满足,清除下一个节点的等待状态,并使用
LockSupport.unpark(s.waiter)
唤醒该节点对应的线程。