【源码解析】从Conditon角度聊聊AQS原理

news2024/11/17 22:35:21

前几篇文章,我们详细描述了线程池的原理以及核心代码,以及阻塞队列,ReentrantLock的核心流程,本篇主要介绍下lock锁中的condition

公平锁和非公平锁

    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

非公平锁

    //非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
             //CAS
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //公平锁 需要判断当前队列是否有线程在等待
if (compareAndSetState(0, acquires)) {} //非公平锁 不需要

非公平锁,会先进行CAS获取锁,抢到了就直接返回。

Condition

condition需要依赖于ReentrantLock,调用await 进入等待或者是singale唤醒,都需要获取锁才可以操作。

一个ReentrantLock可以创建多个condition条件。

    public Condition newCondition() {
        return sync.newCondition();
    }

		final ConditionObject newCondition() {
        return new ConditionObject();
    }

public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
  		 
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;

        /**
         * Creates a new {@code ConditionObject} instance.
         */
        public ConditionObject() { }

condition核心流程其实就是两个,一个是await以及signal。
其实就是两个队列,一个是AQS本身的等待队列(双向链表),另一个就是Condition条件队列。
await() 会将已经获取lock锁的线程,释放锁,然后封装成一个Node节点,添加到条件队列尾部中。挂起。
signal() 将condition队列的头节点移动到AQS等待节点尾部,等待再次获取锁。
在这里插入图片描述

lock.lock();
await();
  // 1.释放当前lock锁,从AQS队列中删除
  // 2.将当前节点封装成Node 添加到条件队列尾部 进行阻塞。 
  // 3.当condition调用siganl的时候,删除在条件队列中的节点,转移到AQS中,等待获取锁。
  // 4.获取到锁之后,执行剩余流程,进行unlock();
lock.unlock();

在这里插入图片描述

 		//前驱节点的引用
        volatile Node prev;
        
        //后继节点的引用
        volatile Node next;
        
        //本线程
        volatile Thread thread;

        // 这个是在condition中用来构建单向链表
        Node nextWaiter;

await

		// 持有锁的线程,执行await
        //1.将当前线程封装为Node,仍到Condition的单向链表中
        //2.将锁资源释放  确认Node没有在双向链表中
        //3.挂起线程
        public final void await() throws InterruptedException {
            //判断线程是否中断 中断异常。
            if (Thread.interrupted())
                throw new InterruptedException();
            //构造一个新的等待队列Node 加入到队尾
            Node node = addConditionWaiter();
            //释放当前线程的独占锁,不管重入几次,state = 0
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //当前节点没有在同步队列上,还没有被singal 将当前线程阻塞
            // 双向链表是否在同步队列,
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                // 被中断直接推出自旋
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
             // 退出上面 自旋 当前节点已经在同步队列中了,
            // 但是当前节点不一定在同步队列队首, acquireQueued将阻塞直到当前节点成为队首
            // 即当前线程获得了锁。然后await()方法就可以退出了,让线程继续执行await()后的代码。
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

节点添加到等待队列

		//尾部方式插入到队尾
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            // 如果条件队列的最后一个节点取消了 清楚
            // 单向链表有值,走这个逻辑
            if (t != null && t.waitStatus != Node.CONDITION) {
                //  这个方法会遍历整个条件队列,然后会将已取消的所有节点清除出队列
                //  将当前Node脱离单向链表
                unlinkCancelledWaiters();
                //  更换新的
                t = lastWaiter;
            }
            // 将当前Node 封装成Node
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            // 当前Node是否为空,放到单线链表中
            // 这个操作持有锁的线程才能做
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

完全释放独占锁

    // 完全释放独占锁
    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            //获取state的值
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

等待进入阻塞队列

            while (!isOnSyncQueue(node)) {
                // 当前线程park 等待singal()
                LockSupport.park(this);
                // 被中断直接推出自旋
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
    // 判断当前节点是否在阻塞队列中
    final boolean isOnSyncQueue(Node node) {
        //  如果当前节点状态是CONDITION或node.prev是null,
        //  则证明当前节点在等待队列上而不是同步队列上。之所以可以用node.prev来判断,
        //  是因为一个节点如果要加入同步队列,在加入前就会设置好prev字段。
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //  如果node.next不为null,则一定在同步队列上,因为node.next是在节点加入同步队列后设置的
        if (node.next != null) // If has successor, it must be on queue
            return true;
            
        //前面的两个判断没有返回的话,就从同步队列队尾遍历一个一个看是不是当前节点。
        return findNodeFromTail(node);
    }

signal

上面的操作,可以看到线程lock.lock 之后被await 阻塞了,那么就需要另外一个线程进行唤醒。

        // 将当前线程唤醒,并且需要仍到AQS双向链表中,同时断开现在存在的单向链表
        public final void signal() {
            // 当前执行singel的线程,必须是持有锁的线程
            if (!isHeldExclusively())
                // 当前执行singal的方法 没有持有锁
                throw new IllegalMonitorStateException();
            // 拿到了第一个被await的Node
            Node first = firstWaiter;
            // 如果没有first 为null , 没await 直接告辞结束
            if (first != null)
                //condition有 ,first节点进行唤醒
                doSignal(first);
        }
        // 从前往后 遍历 找到第一个需要转移的node
        private void doSignal(Node first) {
            do {
                // 判断当前Node是否是唯一的一个节点
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                // 断开存在的单向链表
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
    // 将节点从条件队列转移到阻塞队列
    // true 成功转移
    // false 转移失败
    final boolean transferForSignal(Node node) {
        // 修改当前节点的状态 -2 > 0
        // node在添加的时候 是-2
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            // 修改失败
            return false;

        // sigal 将Node的state -2 -> 0
        // 执行enq方法,放入双向链表的内部 尾部
        Node p = enq(node);
        // 拿到上一个node的状态
        int ws = p.waitStatus;
        // 之前节点是取消状态
        // 节点正常 但是cas
        // 避免刚刚加入的AQS节点 无法被唤醒
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

获取独占锁

上面说了,当signal 将节点从conditon队列加入到阻塞队列中,这里的while循环就会跳出来。

			while (!isOnSyncQueue(node)) {
                // 当前线程park 等待singal()
                LockSupport.park(this);
                // 被中断直接推出自旋
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 退出上面 自旋 当前节点已经在同步队列中了,
            // 但是当前节点不一定在同步队列队首, acquireQueued将阻塞直到当前节点成为队首
            // 即当前线程获得了锁。然后await()方法就可以退出了,让线程继续执行await()后的代码。
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取node的prev节点
                final Node p = node.predecessor();
                // p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
                if (p == head && tryAcquire(arg)) {
                    // //到这里说明刚加入到等待队列里面的node只有一个,并且此时获取锁成功,设置head为node
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // // 到这里,说明上面的if分支没有成功,要么当前node本来就不是队头,
                // // 要么就是tryAcquire(arg)没有抢赢别人,继续往下看
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

小结

总的来说,Condition的本质就是等待队列和同步队列的交互:

当一个持有锁的线程调用Condition.await()时,它会执行以下步骤:

  • 构造一个新的等待队列节点加入到等待队列队尾
  • 释放锁,也就是将它的同步队列节点从同步队列队首移除
  • 自旋,直到它在等待队列上的节点移动到了同步队列(通过其他线程调用signal()) 或被中断
  • 阻塞当前节点,直到它获取到了锁,也就是它在同步队列上的节点排队排到了队首。
    当一个持有锁的线程调用Condition.signal()时,它会执行以下操作:

从等待队列的队首开始,尝试对队首节点执行唤醒操作;如果节点CANCELLED,就尝试唤醒下一个节点;如果再CANCELLED则继续迭代。

对每个节点执行唤醒操作时,首先将节点加入同步队列,此时await()操作的步骤3的解锁条件就已经开启了。然后分两种情况讨论:

  • 如果先驱节点的状态为CANCELLED(>0) 或设置先驱节点的状态为SIGNAL失败,那么就立即唤醒当前节点对应的线程,此时await()方法就会完成步骤3,进入步骤4.
  • 如果成功把先驱节点的状态设置为了SIGNAL,那么就不立即唤醒了。等到先驱节点成为同步队列首节点并释放了同步状态后,会自动唤醒当前节点对应线程的,这时候await()的步骤3才执行完成,而且有很大概率快速完成步骤4.

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

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

相关文章

Liunx系统挂载磁盘

1.具体步骤 大概五个步骤 添加磁盘磁盘分区格式化分区挂载分区到指定目录设置开机自动挂载 目标将sdb1分区挂载到/data目录 2.添加磁盘 使用lsblk -f命令可以查看当前系统磁盘情况 lsblk -f 可以看到已经有一个磁盘sda,现在我们给虚拟机增加一个磁盘 添加完成后…

五、Java核心数组篇

1.数组 概念: ​ 指的是一种容器,可以同来存储同种数据类型的多个值。 ​ 但是数组容器在存储数据的时候,需要结合隐式转换考虑。 比如: ​ 定义了一个int类型的数组。那么boolean。double类型的数据是不能存到这个数组中的&…

什么台灯好用不伤眼睛?适合考公使用的台灯推荐

随着时代的发展与进步,不管是办公族还是学生党的压力也越来越大的,不少人在晚上回去之后仍然需要学习、工作,这样的一件试几乎成为了“家常便饭”,而这个过程中必不可少就是台灯。有些人为了保护眼睛会选择护眼台灯,但…

考研护眼台灯怎么选好?2023考研党台灯分享

现在随着生活水平的提升,孩子的教育也越好越好了,不过随之而来的就是繁重的学习压力,和做不完的作业。细心的家长可能已经发现,自从孩子步入高年级之后,晚上回到家完成作业的时间也越来越长了,这不得不让身…

OfficeWeb365 SaveDraw 文件上传漏洞复现

0x01 产品简介 OfficeWeb365 是专注于 Office 文档在线预览及PDF文档在线预览云服务,包括 Microsoft Word 文档在线预览、Excel 表格在线预览、Powerpoint 演示文档在线预览,WPS 文字处理、WPS 表格、WPS 演示及 Adobe PDF 文档在线预览。 0x02 漏洞概述 OfficeWeb365 Sav…

面相对象开发的原则

1、开闭原则 对修改关闭,对扩展打开。 2、里氏替换原则 子类继承父类的方法时,不可重写父类的方法。 如果重写了父类的方法会导致整个继承体系比较差,特别是运用多态比较平凡时,程序运行出错概率较大。 如果出现了违背“里氏替换…

力扣105与106从前序与中序(中序与后序)遍历序列构造二叉树

本题刚接触时比较懵,做出来后就很好理解。之前数据结构的题:给出中序与后序(或前序与中序)画出该树 无从下笔不知如何去画,做完此题后就发现豁然开朗,不过尔尔。本题只讲中序与后序(前序与中序一…

【设计模式--行为型--命令模式】

设计模式--行为型--命令模式 命令模式定义结构案例优缺点使用场景 命令模式 定义 将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储,传递,调用…

cpp:1:10: fatal error: opencv2/core.hpp: 没有那个文件或目录

前言&#xff1a; 我按照官网方法安装了opencv&#xff0c;运行的也是官网的测试代码&#xff1a; #include <opencv2/core.hpp> #include <opencv2/highgui.hpp> using namespace cv; int main() {printf("hello world")return 0; } 半解决&#xff…

TCP/IP详解——TCP 协议

文章目录 一、传输层协议1. TCP1.1 TCP 的字节流1.2 TCP 端口号1.3 TCP 头部1.4 TCP 选项部分字段1.5 TCP 三次握手1.6 TCP 三次握手不成功1.6.1 TCP 拒绝&#xff08;被RST重置&#xff09;1.6.2 TCP 半连接1.6.3 TCP 连接无响应 1.7 TCP 传输过程及原理1.7.1 TCP 传输过程1.7…

高通开发系列 - 功耗问题之添加CPU Idle和Hotplug的功能

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 返回:专栏总目录 目录 概述CPU IdleCPU TopologyCPU Idle DriverCPU Idle GovernorCPU的hotplug函数

给我说说Redis持久化机制RDB吧

基础 了解过Redis持久化RDB嘛&#xff1f;可不可以解释一下什么是RDB。 答&#xff1a; RDB持久化机制是将内存中的数据生成快照并持久化到磁盘的过程&#xff0c;RDB可以通过手动或者自动的方式实现持久化。 那RDB触发的方式有哪几种方式知道吗&#xff1f; 答: 有两种&am…

【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统(三)日志管理(登录日志、操作日志)、用户登录模块

第一篇&#xff1a;【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统&#xff08;一&#xff09;搭建项目 第二篇&#xff1a;【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统&#xff08;二&#xff09;日志输出中间件、校验token中间件、配置路由、基础工具函数。 …

赛氪为第五届全球校园人工智能算法精英大赛决赛选手保驾护航

12月10日&#xff0c;以“智青春算未来”为主题的2023年第五届全球校园人工智能算法精英大赛全国总决赛在河海大学江宁校区举行。本次大赛由江苏省人工智能学会主办&#xff0c;自9月份启动以来&#xff0c;共吸引了全国近400所高校的3000多支参赛团队参加。经过校赛、省赛选拔…

Linux(统信UOS) 发布.Net Core,并开启Https,绑定证书

实际开发中&#xff0c;有时会需要为小程序或者需要使用https的应用提供API接口服务&#xff0c;这就需要为.Net Core 配置https&#xff0c;配置起来很简单&#xff0c;只需要在配置文件appsettings.json中添加下面的内容即可 "Kestrel": {"Endpoints": …

LeetCode力扣每日一题(Java)66、加一

每日一题在昨天断开了一天&#xff0c;是因为作者沉迷吉他&#xff0c;无法自拔……竟然把每日一题给忘了&#xff0c;所以今天&#xff0c;发两篇每日一题&#xff0c;把昨天的给补上 一、题目 二、解题思路 1、我的思路 其实乍一看这道题还是比较简单的&#xff0c;就是让…

C/C++ 汇总区间-返回恰好覆盖数组中所有数字的最小有序区间范围列表

给定一个无重复元素的有序数数组ums。返回恰好覆盖数组中所有数字的最小有序区间范围列表。 也就是说&#xff0c;nums 的每个元素都恰好被某个区间范围所覆盖&#xff0c;并且不存在属于某个范围但不属于 nums 的数字x。 列表中的每个区间范围 [a,b] 应该按如下格式输出&…

zabbix——实现高效网络监控

在当今的数字化时代&#xff0c;网络和服务器的健康状况对于企业的正常运营至关重要。为了及时发现和解决潜在的问题&#xff0c;许多企业选择使用网络监控工具来追踪服务器的性能和网络参数。其中&#xff0c;Zabbix是一个功能强大且开源的网络监控工具&#xff0c;被广泛应用…

unity 2d 入门 飞翔小鸟 死亡闪烁特效(十三)

一、c#脚本 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Bling : MonoBehaviour {public Texture img;public float speed;public static bool changeWhite false;private float alpha0f;// Start is called before the fi…

EasyRecovery(数据恢复软件) 2024中文绿色无需激活版下载

EasyRecovery是一款功能强大且专业的数据恢复软件&#xff0c;软件能够对电脑误删的文件进行恢复&#xff0c;包括格式化硬盘是数据恢复、手机U盘数据恢复等&#xff0c;小编今天给大家带来的是根据官软件解压后直接使用。感兴趣的朋友快来下载使用吧。 EasyRecovery-2024mac最…