被问到可重入锁条件队列,看这一篇就够了!|原创

news2025/2/25 15:26:17

本文深入解读了高频面试点——ReentrantLock的条件队列使用方法及其原理。源码有详细注释,建议收藏阅读。

点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达

Jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上两者没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景,其原理之前已经介绍过,请自行阅读。

0910213ec0fc3a10f360657aa47fcab3.jpeg

重点,一文掌握ReentrantLock加解锁原理!|原创


使用synchronized结合Object上的waitnotify方法可以实现线程间的等待通知机制。ReentrantLockCondition同样可以实现这个功能,而且相比前者使用起来更清晰也更简单。前者是java底层级别的,后者是语言级别的,后者可控制性和扩展性更好。

Condition与Object的wait/notify区别

  1. Condition能够支持不响应中断,而通过使用 Object 方式不支持

  2. Condition能够支持多个等待队列(new 多个Condition对象),而 Object 方式只能支持一个

  3. Condition能够支持超时时间的设置,而 Object 不支持

使用示例

为了方便理解源码,我们先用一个Demo展示一下ReentrantLock的线程停止和通知是如何使用的。这里使用的是一个生产者和消费者的模型,一个线程负责加,另一个线程负责减。

static volatile int i = 0;
static final ReentrantLock LOCK = new ReentrantLock();
static final Condition condition = LOCK.newCondition();

public static void add() throws InterruptedException {
    LOCK.lock();
    try {
        while (i == 0) {
            Thread.sleep(1000);
            System.out.print("add\t");
            System.out.println(++i);
            condition.signal();
            condition.await();
        }
    } finally {
        LOCK.unlock();
    }
}

public static void sub() throws InterruptedException {
    LOCK.lock();
    try {
        while (i == 1) {
            Thread.sleep(1000);
            System.out.print("sub\t");
            System.out.println(--i);
            condition.signal();
            condition.await();
        }
    } finally {
        LOCK.unlock();
    }
}

public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
        while (true) {
            try {
                add();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
    new Thread(() -> {
        while (true) {
            try {
                sub();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

可以看到,想要获得一个Condition对象,需要首先通过一个ReentrantLock锁来创建,而最终调用其实为AQS中的内部类ConditionObject。

condition是要和lock配合使用的,而lock的实现原理又依赖于AQS,所以AQS内部实现了ConditionObject。我们知道在锁机制的实现上,AQS内部维护了一个双向的同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列。condition内部也是使用相似的方式,内部维护了一个单向的等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。

4f5cd36022ac9d50e8352838347e0e36.png

ConditionObject中有两个成员变量:头节点firstWaiter 和 尾节点lastWaiter ,同步队列的成员Node 复用了实现同步队列的内部类Node。用nextWaiter保存了下一个等待节点,源码如下。

Condition condition = LOCK.newCondition();
//ReentrantLock内部类Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
    final ConditionObject newCondition() {
        return new ConditionObject();
    }
}
// AQS内部类 ConditionObject
public class ConditionObject implements Condition, java.io.Serializable {
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;

    //真正的创建Condition对象
    public ConditionObject() { }
}
static final class Node {
  Node nextWaiter;
}

用Object的方式Object对象监视器上只能拥有一个同步队列和一个等待队列,而使用Lock可以有有一个同步队列和多个等待队列。可以多次调用lock.newCondition()创建多个Condition,所以一个Lock可以持有多个等待队列。

22418f15572bff59f199aeb81505856d.png

下面开始解读await()signal()方法。

Await方法原理

阻塞前:

1.在条件队列尾部添加新节点(状态CONDITION=-2),如果头节点为空则把当前节点设为头节点。

2.获取当前线程占有的state,无论state是几,都清空为0,代表完全释放锁。并且在释放当前线程所占用的锁之后,会唤醒同步队列中的下一个节点。

3.进入自旋判断逻辑:如果当前节点状态是 CONDITION(-2)或者 prev 节点(表示在同步队列中有前驱节点)为空,返回false,进入while逻辑,阻塞当前线程;如果有继承者,表示肯定在同步队列中,直接跳出循环;如果从同步队列队尾开始寻找,找到当前节点,同样表示在队列中,跳出循环。

bbc301f1fd2ef3345c6be2497a51f18f.png

注意!! 是先添加到条件队列,再释放锁。所以有可能出现以下的情况,A插入条件队列调用await唤醒B,但是在A唤醒后准备park时,B已经执行完需要的逻辑,并且再次Park。此时的A线程可能已经状态不再是CONDITION,说明已经进入同步队列,那就可以跳过Park再次直接争夺锁,所以这里需要自旋锁去不断尝试判断。

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 1. 添加新节点,将当前线程保存其中,并且添加到等待队列队尾
    Node node = addConditionWaiter();
    // 2. 释放当前线程所占用的lock,并且唤醒同步队列中的下一个节点
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 当不在同步队列中(处于condition状态或者前一个节点为null)
    while (!isOnSyncQueue(node)) {
        // 3. 当前线程进入到等待状态
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 4. 自旋等待获取到同步状态(即获取到lock)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        //删除无效的等待节点
        unlinkCancelledWaiters();
    // 5. 处理被中断的情况
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

阻塞后:

  1. 恢复执行后,检查是否中断。然后自旋再次判断是否已经进入同步队列,返回true,跳出循环继续执行。

  2. 调用acquireQueued,尝试去争夺锁,这里逻辑和lock一样,已经是同步队列去竞争锁的逻辑。并且会将之前清空的state值按照原来的大小设置。

  3. 最后都是一些中断标记的处理,主流程已经结束。

注意:退出await方法一定表明当前线程已经获得了与condition关联的锁资源。

c3373e755fa4106bdae7529903b9b55e.png

具体请看代码:

// AQS
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 1. 添加新节点,将当前线程保存其中,并且添加到等待队列队尾
    Node node = addConditionWaiter();
    // 2. 释放当前线程所占用的lock,并且唤醒同步队列中的下一个节点
    int savedState = fullyRelease(node);
    int interruptMode = 0;
  
  //是先添加到等待队列,再释放锁。所以有可能出现以下的情况,A插入条件队列调用await唤醒B,但是在A唤醒后准备park时,B已经执行完需要的逻辑,并且再次Park,此时的A就可以跳过Park再次直接争夺锁。
    while (!isOnSyncQueue(node)) {
        // 3. 关键节点!!!:当前线程进入到等待状态
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 4. 自旋等待获取到同步状态(即获取到lock)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
  
  // 如果节点线程被取消才会进入这里的逻辑。正常不会
    if (node.nextWaiter != null) // clean up if cancelled
        //删除无效的等待节点
        unlinkCancelledWaiters();
    // 5. 处理被中断的情况
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
// 添加新的条件队列节点
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 清除被取消的尾节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        //解除关联
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //将当前线程保存在Node中
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        //队尾插入
        t.nextWaiter = node;
    //更新lastWaiter (如果是第一次插入节点,头尾节点都是同一个)
    lastWaiter = node;
    return node;
}
//完全释放锁状态
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
      // 这里会释放锁,并且唤醒后继节点
        if (release(savedState)) {
            //成功释放同步状态
            failed = false;
            return savedState;
        } else {
            //不成功释放同步状态抛出异常
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

Signal

  1. 检查本线程是否持有锁,正常是持有锁,如果不符合就抛出异常。

  2. 从等待队列中拿到第一个节点。如果头节点为空代表条件队列为空,谁也不通知直接结束。

  3. 将头节点从条件队列中移除,并且把nextWaiter置为null。然后把节点状态设为0,转移进入同步队列。如果队列为空则初始化同步队列。

  4. 如果前驱节点不是 signal 状态或者前一个节点已经被取消,直接对头节点线程解除阻塞。返回true跳出循环。

  5. 至此本线程方法执行结束。依旧持有锁,但是转移了条件队列的头节点到同步队列中,就做了这一件事。

//AQS
public final void signal() {
    //1. 先检测当前线程是否已经获取lock
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

//ReentrantLock
protected final boolean isHeldExclusively() {
    // While we must in general read state before owner,
    // we don't need to do so to check if current thread is owner
    return getExclusiveOwnerThread() == Thread.currentThread();
}

//AQS
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        //1. 将头结点从等待队列中移除
        first.nextWaiter = null;
        //2. while中transferForSignal方法对头结点做真正的处理
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
    //1. 更新状态为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    //2.将该节点移入到同步队列中去
   // 这里的处理和同步队列的生成用的同一个方法
   // node p 为前驱节点(原尾节点)
    Node p = enq(node);
    int ws = p.waitStatus;
   // 如果前驱节点不是signal状态或者前一个节点已经被取消,直接对头节点解除阻塞。返回true跳出循环
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

具体原理图如下:

feb70e6bbb0d750292ddad22c311e539.png

SignalAll

signalAll与signal方法的区别体现在doSignalAll方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,而doSignalAll将条件队列中的所有Node都转移到了同步队列中,即“通知”当前调用condition.await()方法的每一个线程,代码如下。

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

最后,欢迎大家提问和交流。

如果对你有帮助,欢迎点赞、评论或分享,感谢阅读!

update在MySQL中是怎样执行的,一张图牢记|原创

2022-11-19

eb697c182f74ee74d74b1d09d9bb5a23.jpeg

讲真,这篇最全HashMap你不能错过!|原创

2022-11-17

a96ce6f765bfa33ec4ee251352719e62.jpeg

MySQL主从数据不一致,怎么办?

2022-11-15

6e263aa5fa9758bc7a664efe60d69195.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/39629.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用HTML+CSS做一个简单的新闻门户 1页网页

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材,DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 茶文化网站 | 中华传统文化题材 | 京剧文化水墨风书画 | 中国民间年画文化艺术网站 | 等网站的设计与制作 | HTML期末大学生网页设计作业,…

Web前端大作业—电影网页介绍8页(html+css+javascript) 带登录注册表单

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置,有div的样式格局,这个实例比较全面,有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 精彩专栏推荐&#x1f4…

DevOps初学者的指南——阿里出品学习图册带你掌握高薪技术!

开篇 你是否想开始学习DevOps,或者愿意通过增加DevOps这一技能来转变你的职业生涯? 如果你的答案是肯定的,那么你就来对地方了 从初创企业到跨国企业,技术行业的每个细分领域都在改变其软件开发方法。DevOps工具和实践惊人地减…

【mysql 高级】explain的使用及explain包含字段的含义

explain的使用及explain包含字段的含义1.id2. select_type3.table4.type5.possible_keys6.key7.key_len8.ref9.rows10.Extra使用explain关键字可以模拟优化器执行SQL语句,从而知道MySQL是如何处理你的SQL语句的,从而分析你的查询语句或是表结构的性能瓶颈…

面向OLAP的列式存储DBMS-10-[ClickHouse]的常用数组操作

参考ClickHouse 中的数据查询以及各种子句 ClickHouse 数组的相关操作函数,一网打尽 在关系型数据库里面我们一般都不太喜欢用数组,但是在 ClickHouse 中数组会用的非常多,并且操作起来非常简单。ClickHouse 里面提供了非常多的函数&#x…

文本生成视频Make-A-Video,根据一句话就能一键生成视频 Meta新AI模型

Meta公司(原Facebook)在今年9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。 Make-A-Video研究基于文本到图像生成技术的最新进展,该技术旨在实现文本到视频的生成,可…

[附源码]Python计算机毕业设计高校第二课堂管理系统

项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等等。 环境需要 1.运行环境:最好是python3.7.7,…

第十章 二叉树的各种遍历

第十章:二叉树的前、中、后序遍历前期准备:一、前序遍历1、遍历的思路2、遍历代码3、遍历图示二、中序遍历1、遍历的思路2、遍历代码三、后序遍历1、遍历的思路2、遍历代码三、遍历的应用1、计算二叉树中的节点个数2、二叉树叶子节点的个数3、二叉树的深度4、二叉树…

EMC原理-传导(共模、差模)与辐射(近场、远场)详解

目录: 第一章、EMC概念介绍 第二章、感应干扰(近场) 第三章、辐射干扰(远场) 第四章、差模干扰 第五章、共模干扰 ------------------------------------------------------------------------------------------------------------------------ 第一章、EMC…

ceph块存储在线扩容

记录:339 场景:在CentOS 7.9操作系统上,部署ceph-13.2.10集群。应用ceph块设备(ceph block device);主要是创建ceph块存储和在线扩容相关操作。 版本: 操作系统:CentOS 7.9 ceph版本:ceph-1…

牛顿法(牛顿拉夫逊)配电网潮流计算matlab程序

牛顿法配电网潮流计算matlab程序 传统牛顿—拉夫逊算法,简称牛顿法,是将潮流计算方程组F(X)0,进行泰勒展开。因泰勒展开有许多高阶项,而高阶项级数部分对计算结果影响很小,当忽略一阶以上部分时,可以简化对…

向NS-3添加新模块_ns3.37添加新模块_ns3.37不同版本模块移植

使用ns3的时候,我们需要调用很多模块,比如对wifi的简单功能进行仿真时: ns-3.35_third.cc_ns-3网络仿真工具wifi脚本解析_wifi脚本网络拓扑_ns-3third脚本全注释_基础ns-3_ns-3入门_ns-3third脚本解析_Part1_Mr_liu_666的博客-CSDN博客Intro…

WinSock的I/O模型

目录 一、 套接字的非阻塞工作模式 1.阻塞与非阻塞模式的概念 2.阻塞模式下能引起阻塞的套接字函数 3.两种模式的比较 2. 套接字非阻塞模式的设置方法——ioctlsocket 函数 3. 非阻塞模式下的编程方法 4. 非阻塞模式服务器端程序和客户端程序 二、select模型 1. 套接字…

信息系统综合测试与管理

本文包括软件测试模型、测试技术和测试管理。 一、测试基础 1、软件测试模型 所谓测试模型(Test Model),是测试和测试对象的基本特征、基本关系的抽象。 1)V模型 V模型实际是软件开发瀑布模型的变种,它反映了测试…

CSDN第十期竞赛

比赛详情: 通过这次的周赛让我受益颇多,这次的题目都是平常练习题目的变形,这次的竞赛是十分有意义的,加强对练习题的强化。 两道模拟题: 目录 1.熊孩子拜访 2.走楼梯 1.熊孩子拜访 题目描述 已知存在一个长度为n的…

FB显示学习期数据不足怎么办?

组合投放广告组和广告系列。组合投放广告组和广告系列有助于加快获得所需成效的速度,这意味着广告投放后很快便可看到稳定的成效。 扩大受众群。受众越多,用户完成您的优化事件的机会越多。 提高预算。如果您的预算过低,无法获得约 50 个​…

47、泛型

一、引入 1、传统方法: package generic_;import java.util.ArrayList; SuppressWarnings({"all"}) public class Generic01 {public static void main(String[] args) {ArrayList arrayListnew ArrayList();arrayList.add(new Dog("旺财",10)…

Spring项目结合Maven【实现读取不同的资源环境】

📃目录跳转📚简介:🍑修改pom.xml🥞修改application.yml🚀 演示:📚简介: 由于我们写功能的不能影响到线上环境的配置,所以每一次增加功能我们都要吧项目部署到…

STM32HAL库单ADC+DMA学习及测试记录

一、打开STM32CubeMX选择对应的芯片型号,后进入开发界面 1、双击“STM32CubeMX”软件打开,打开后如下图所示,选择红色框; 选择后可能会更新,等待更新完成即可,如下图: 2、选择开发芯片&#x…

【剧前爆米花--爪哇岛寻宝】面向对象的三大特性——封装、继承以及多态的详细剖析(上——继承)。

作者:困了电视剧 专栏:《JavaSE语法与底层详解》 文章分布:这是一篇关于Java面向对象三大特性——继承的文章,在本篇文章中我会分享继承的一些基础语法以及类在继承时代码的底层逻辑和执行顺序。 目录 继承 需求 继承的定义及语…