当前位置: 首页 > news >正文

并发编程-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);}
}
  1. 排队

    • addWaiter(Node.EXCLUSIVE):将当前线程封装成 Node,加入 AQS 的同步队列尾部。

  2. 自旋+挂起

    • 循环判断:只有当自己是队列头的下一个节点(p == head)并且能 tryAcquire 成功,才出队并返回;

    • 否则,通过 parkAndCheckInterrupt() 挂起自己。

  3. 中断检测

    • parkAndCheckInterrupt() 返回 true 表示线程在挂起期间被中断,此时 立刻 抛出 InterruptedException,并在 finally 中进行当前节点的清理(cancelAcquire),退出获取流程。

private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}

这里的interrupted除了返回当前中断状态,还可以清除中断状态,让下一次调用park方法生效

4. 整体流程小结
  1. 调用 lockInterruptibly()acquireInterruptibly(1)

  2. 快速路径tryAcquire(1) 成功则立即返回;

  3. 阻塞路径doAcquireInterruptibly(1)

    • 排入同步队列

    • 循环尝试唤醒(head 节点出队后依次轮到自己)

    • 每次挂起前后都检查中断 —— 一旦被中断,立刻抛 InterruptedException 并清理自身排队节点

  4. 退出

    • 成功获取: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 的时序:

  1. A 执行完一次,turnA = false,唤醒 B;

  2. B 还没来得及执行,A 抢到了锁(因为操作系统调度是随机的);

  3. A 进入下一轮,判断 !turnA[0] == true

  4. 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;
}
  1. 获取当前线程:首先获取当前线程的引用。

  2. 尝试获取锁

    • 使用 compareAndSetState(0, 1) 尝试将锁的状态从0更新为1。

    • 如果成功,设置当前线程为锁的持有者,并返回 true

3. 可重入与加锁失败(可重入特点)

  1. 检查是否已经持有锁

    • 如果锁已经被当前线程持有,允许重入。

    • 获取当前锁的状态并加1,检查是否发生溢出。

    • 果没有溢出,更新锁的状态,并返回 true

  2. 返回失败

    • 如果以上条件都不满足,表示当前线程无法获取锁,返回 false

这段代码确保了锁的获取过程是线程安全的,并且支持锁的重入。

解锁流程

1  调用AQS的释放锁方法

AQS的释放锁 release方法

public final boolean release(int arg) {if (tryRelease(arg)) {signalNext(head);return true;}return false;}

参数arg是释放操作的参数,通常表示锁的持有次数。

逻辑

  1. 调用tryRelease(arg)尝试释放锁。

  2. 如果tryRelease返回true,表示锁成功释放。

  3. 返回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表示要释放的锁的持有次数。

逻辑

  1. 计算新的状态值c,即当前状态减去释放次数。

  2. 检查当前线程是否是锁的持有者,如果不是则抛出IllegalMonitorStateException异常。

  3. 如果新的状态值c为0,表示锁完全释放,设置锁的持有线程为null

  4. 更新状态值为c

  5. 返回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是等待队列的头节点。

逻辑

  1. 检查头节点是否为空,以及头节点的下一个节点是否不为空且其等待状态不为0。

  2. 如果条件满足,清除下一个节点的等待状态,并使用LockSupport.unpark(s.waiter)唤醒该节点对应的线程。

相关文章:

  • Git:现代开发的版本控制基石
  • 高效解决Java内存泄漏问题:方法论与实践指南
  • 《信号与系统》第 9 章 拉普拉斯变换
  • npm安装electron报错权限不足
  • swm341s map文件和sct文件解析
  • arcsin x
  • 一阶低通滤波器完整推导笔记
  • 斗式提升机的负载特性对变频驱动的要求
  • 声波下的眼睛:用Python打造水下目标检测模型实战指南
  • Android 中 linux 命令查询设备信息
  • 阳台光伏新风口!安科瑞ADL200N-CT/D16-WF防逆流电表精准护航分布式发电
  • 完美解决openpyxl保存Excel丢失图像/形状资源的技术方案
  • 几种经典排序算法的C++实现
  • 软考高级系统规划与管理师备考经验
  • Atlassian AI(Rovo)在不同场景中的实际应用:ITSM、HR服务、需求管理、配置管理
  • 26考研 | 王道 | 计算机组成原理 | 五、中央处理器
  • 心之眼 豪华中文 免安 离线运行版
  • OB Cloud × 海牙湾:打造高效灵活的金融科技 AI 数字化解决方案
  • Rocky Linux 9 系统安装配置图解教程并做简单配置
  • 【6S.081】Lab2 System Calls
  • 申请网页要多少钱/企业网站seo推广
  • 总部基地网站建设/微信引流推广怎么做
  • 网站建设发票内容/跨境电商营销推广
  • 苏州相城做网站的/最新足球消息
  • 做网站是什么职位/公众号seo排名
  • 图片网站建设/百度关键词热度查询工具