深入底层源码,剖析AQS的来龙去脉!

news2024/10/2 20:34:33

这里写目录标题

  • 回顾
  • 前缀知识
  • 一、Condition的概念
  • 二、Condition底层结构
  • 三、Condition源码解析
      • 3.1 newCondition()
      • 3.2 await()
  • 总结
    • 主要方法:

回顾

如果你还没熟悉 AQS 中的独占锁,可以先看这篇文章的前导篇。上一篇文章是以ReentrantLock 里面的加锁、解锁源码进行分析的。

文章链接:(一)从底层源码剖析AQS的来龙去脉!

回顾上文,其实公平锁和非公平锁的实现区别无非两点。

首先,非公平锁在调用lock后,直接使用CAS进行抢锁操作,如果锁未被占用,则直接获取锁并返回。

然后,非公平锁在CAS失败后,会进入tryAcquire方法,如果发现锁被释放了(state == 0),非公平锁会再次尝试通过CAS抢锁;而公平锁则会检查等待队列是否有线程处于等待状态,如果有等待线程,公平锁不会抢锁,而是将自己加入队列尾部,遵循先来先得的原则。

其实大体上就是这两点区别,而且如果这两次的 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都需要进入到阻塞队列等待唤醒。

tips:如果想要更好的理解此篇文章,希望大家可以好好看一下这篇文章,否则这篇文章看着应该会很痛苦的。

文章链接:(一)从底层源码剖析AQS的来龙去脉!

前缀知识

AQS 内部存在的两种类型的队列:
  1. 同步队列:这是线程在等待锁时所处的队列,即当线程获取锁资源发现已经被其他线程占有而加入的队列;
  2. 等待队列(可能有多个等待队列):这是由 ConditionObject 维护的队列,用于存放调用 await()方法而释放锁并等待信号的线程。当线程被 signal()或signalAll()方法唤醒时,它将从等待队列中移除,并重新加入同步队列以竞争锁。

两种队列的概念:
  1. 同步队列是线程等待锁的队列,其中的线程处于阻塞状态,等待锁的释放以便获取锁。
  2. 等待队列是一个特定于每个 ConditionObject 的队列,用于存放调用 await() 方法被阻塞的线程。

一、Condition的概念

Condition是一个接口类,具体实现者为AQS内部的ConditionObject类。

AQS使用内部类ConditionObject构建等待队列,当Condition调用await()方法后,等待获取锁资源的线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移到同步队列中进行锁资源的竞争。

Condition 经常可以用在生产者-消费者的场景中,请先看下面这个例子。

场景描述:

模拟短信发送和接收的过程。在这个场景中,生产者是短信发送者,消费者是短信接收者。现在通过一个队列来存储待发送和接收的短信。

这其中包含一个队列来保存消息,一把锁 ReentrantLock 用于线程同步,以及两个条件变量,分别用于短信发送者和短信接收者。

代码实现

public class MessageQueue {
    private LinkedList<String> queue = new LinkedList<>();
    private final int capacity = 10;
    
    private final ReentrantLock lock = new ReentrantLock(); 
    // condition 依赖于 lock 来产生
    private final Condition notFull = lock.newCondition(); // 生产者条件
    private final Condition notEmpty = lock.newCondition(); // 消费者条件

    public void put(String message) throws InterruptedException {
        lock.lock();
        try {
            // 如果队列已满,则生产者等待
            while (queue.size() == capacity) {
                notFull.await();
            }
            queue.addLast(message);
            System.out.println("放入消息: " + message);
            notEmpty.signal(); // 唤醒消费者
        } finally {
            lock.unlock();
        }
    }

    public String take() throws InterruptedException {
        lock.lock();
        try {
            // 如果队列为空,则消费者等待
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            String message = queue.removeFirst();
            System.out.println("取出消息: " + message);
            notFull.signal(); // 唤醒生产者
            return message;
        } finally {
            lock.unlock();
        }
    }
}

通过上面的例子其实可以得到结论:使用 Condition 时,必须先持有相应的锁

作用

每个Object对象都会在被创建的时候与一个监视器对象产生“羁绊”,而每个对象也会有一组监视器的方法,即wait()/notify() 或 notifyAll() 方法, 通过这些可以实现线程之间的通信机制,也就是等待/唤醒机制。

但这些有个前提,那就是需要持有对象的监视器锁才可以调用wait()/notify() 或 notifyAll() 方法,但是相对于notify()的随机唤醒等待锁的一个线程,和 notifyAll()直接唤醒所有等待锁的线程,Condition的await()/signal()无疑更加准确,他们可以精确唤醒某个等待锁的线程。

并且他们等待、唤醒机制不同,在monitor监视器中,一个对象有一个同步队列和一个等待队列,但是在AQS中一个锁对象则是可以拥有一个同步队列和多个等待队列

注意:wait(), notify()这些方案是基于对象监视器锁的,而 Condition 是基于 ReentrantLock 实现的。所以不管是调用 await 进入等待还是 signal唤醒,必须获取到锁才能进行操作!

二、Condition底层结构

2.1 AQS底层基本变量

我们先看一下AQS底层基本变量:

private transient volatile Node head; // 头结点, 可以理解为当前当前持有锁的线程
 
private transient volatile Node tail; // 尾节点,可以理解为被阻塞的线程节点

private volatile int state; // 表示共享资源(可以理解为是否获取到锁的标志)

private transient Thread exclusiveOwnerThread; // 表示当前占据锁的线程

2.2 Node节点结构

abstract static class Node {
    volatile Node prev;       // 表示前驱指针
    volatile Node next;       // 表示后继指针
    Node nextWaiter;		  // 等待队列里下一个等待条件的结点
    volatile Thread thread;    // 线程本程
    
	/* 共享还是独占模式的标识 */
	// 共享模式时的节点标识
	static final Node SHARED = new Node();
	// 独占模式时的节点标识
    static final Node EXCLUSIVE = null;    

    /* 下列四个变量表示Node在队列中的状态 waitStatus专用 */
    volatile int waitStatus;  // 节点的等待状态
    static final int CANCELLED =  1; // 表示线程获取锁请求已经取消(线程等待超时或被中断~)
    static final int SIGNAL    = -1; // 表示线程需要被唤醒,等待资源释放
    static final int CONDITION = -2; // 表示节点在条件队列中,等待某个条件的满足
    static final int PROPAGATE = -3; // 在共享模式下,表示后续节点需要被唤醒并继续执行
}

2.3 FIFO同步队列

在这里插入图片描述
注意:阻塞队列不包含 head,同步队列则是包含head!!!

PS:上面结构如果不是太清楚是作用是什么,可以去我的上一篇文章理清楚。

文章链接:(一)从底层源码剖析AQS的来龙去脉!

2.4 ConditionObject

Condition只是一个接口,具体实现是AQS内部类的ConditionObject

// 条件队列头节点
private transient Node firstWaiter;
// 条件队列尾节点
private transient Node lastWaiter;

2.5 条件队列

在这里插入图片描述
2.6 队列之间的关系

在这里插入图片描述
图片解释:

1、可以看到不论是同步队列还是条件队列,他们的节点都是Node节点,这个其实是因为最终条件队列的节点还是要去转移到同步队列中的。

2、一个 ReentrantLock 实例可以有一个同步队列+N个条件队列,至于等待队列的N具体是多少,那么则是看我们ReentrantLock实例调用多少次调用 newCondition() 了,其中N >= 0

3、每个 condition 有对应的条件队列,如线程 A 调用 conditionA.await() 方法可以将当前线程 A 封装成 Node ,然后添加到条件队列,继而阻塞在这,等待着唤醒才回去重新进入同步队列,然后获取锁。

4、当调用conditionA.signal() 触发唤醒时,唤醒的是队头,会将conditionA 对应的条件队列的 firstWaiter 移到同步队列的队尾,然后等待获取锁。只有当获取锁后 await() 方法才能返回,继续执行await()方法之后的代码。

为什么只有当获取到锁,才从await()方法返回,并继续往下执行?

因为调用await()的目的就是等待某个条件满足,而这个条件通常是可能会被持有相同锁的其他线程中被改变的。所以为了线程在条件满足时能安全地继续执行,它必须重新获得之前释放的锁。这是为了确保对共享资源的操作是线程安全的,防止竞态条件的发生。

为什么要继续执行await()方法之后的代码?

因为我们既然之前调用了 await() ,那么就证明肯定是由于某些条件的确实,不得不把它阻塞住,从而它后面的代码也执行。所以当我们调用 signal() 的时候,那么证明我们此时的条件满足了,所以也可以执行后面的方法了,所以线程需要返回,然后继续执行 await() 方法之后的代码。

在这个过程中,条件队列的firstWaiter不会立即变成条件队列的下一个节点。只有当被移到同步队列的线程成功获取锁并且await()方法返回之后,条件队列的下一个节点才会成为新的firstWaiter。

所以才能够满足了互斥、顺序以及正确这三个特性:

互斥:重新获取锁可以确保线程在访问共享资源时的互斥性,避免多个线程同时访问共享资源而导致的不一致性。
顺序:重新获取锁后,线程才能确定条件确实已经满足,可以安全地继续执行。
正确:线程在等待条件和重新检查条件之间是互斥的,防止在检查条件时其他线程改变了条件状态。

5、条件队列是一个单向链表,而同步队列则是双向链表,因为它需要通过前后指针来进行唤醒和得知waitStatus状态

注意:队列中的Node节点的waitStatusCONDITION状态。

上面的 2 -> 3 -> 4 只是最简单的流程,没有其他的因素。比如中断、signalAll()以及await(long time, TimeUnit unit)这种带超时方法

具体为什么可以看上篇文章:(一)从底层源码剖析AQS的来龙去脉!。

三、Condition源码解析

3.1 newCondition()

因为Condition 是基于 ReentrantLock 实现的。所以不管是调用 await 进入等待还是 signal唤醒,必须获取到锁才能进行操作,所以我们先看看它们之间是如何实现的。

之前说过,Condition 只是一个接口,它的实现类为 ConditionObject,而每个 ReentrantLock 实例可以通过调用多次 newCondition 产生多个 ConditionObject 的实例。

所以我们首先来看下我们关注的 Condition 的实现类 AbstractQueuedSynchronizer 类中的 ConditionObject是如何创建的的?

// 先获取锁的类型【这点无关紧要,因为不会根据锁的类型而创建不同的obj】,然后去调用重载方法
public Condition newCondition() {
	return sync.newCondition();
}
// 获取ConditionObject实例
final ConditionObject newCondition() {
    return new ConditionObject();
}

很简单啊很简单,就只是创建一个ReentrantLock,然后通过它调用newCondition()就可以了。

ReentrantLock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();

3.2 await()

接下来,我们分析await()方法,源码如下:

// 调用await方法会进行阻塞,直到调用signal方法,而且因为它也可以响应中断,所以被中断的时候,它直接抛出异常。
public final void await() throws InterruptedException {
    // 既然可以响应中断,所以直接判断线程中断状态,如果响应中断直接抛异常中断
    if (Thread.interrupted())
      throw new InterruptedException();
      
    // 把线程封装进节点。然后添加到condition条件队列中
    Node node = addConditionWaiter();
    
    // 释放当前线程持有锁资源,保存 释放锁之前 的state值
    int savedState = fullyRelease(node);
    
    // 用来存储线程中断检查的结果。
    int interruptMode = 0;
    // 判断节点是否在同步队列(SyncQueue)中,即是否被唤醒
    // 情况1:isOnSyncQueue(node)为true,也就是说当前node节点已经转移到同步队列了,已被唤醒。
    // 情况2:(interruptMode = checkInterruptWhileWaiting(node)) != 0,表示线程中断
    while (!isOnSyncQueue(node)) {
      // 如果当前线程不在同步队列中,park阻塞当前线程,直到被unpark方法唤醒,或线程被中断。
      LockSupport.park(this);
      if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
          break;
    }
    // 线程被唤醒后尝试获取锁要。
    // 是获取锁成功后并且没有抛中断异常,那么就 interruptMode = REINTERRUPT; 表示需要重新抛出中断。
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
      interruptMode = REINTERRUPT;
      // 检查条件队列中是否还有其他节点,要是有的话,那么就把他们移除。
      // 也就是说:把条件队列中不是CONDITION状态的节点clear
    if (node.nextWaiter != null) 
      unlinkCancelledWaiters();
    if (interruptMode != 0)
      reportInterruptAfterWait(interruptMode);
}

总结

主要方法:

在这里插入图片描述

public interface Condition {
    /**
    * 调用当前方法会使当前线程处于等待状态直到被通知(signal)或中断
    * 当其他线程调用singal()或singalAll()方法时,当前线程将被唤醒
    * 当其他线程调用interrupt()方法中断当前线程等待状态
    * await()相当于synchronized等待唤醒机制中的wait()方法
    */
    void await() throws InterruptedException;
    
    /**
    * 作用与await()相同,但是该方法不响应线程中断操作
    */
    void awaitUninterruptibly();
    
    /**
    * 作用与await()相同,但是该方法支持超时中断(单位:纳秒)
    * 当线程等待时间超出nanosTimeout时则中断等待状态
    */
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    
    /**
    * 作用与awaitNanos(long nanosTimeout)相同,但是该方法可以声明时间单位
    */
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    
    /**
    * 作用与await()相同,在deadline时间内被唤醒返回true,其他情况则返回false
    */
    boolean awaitUntil(Date deadline) throws InterruptedException;
    
    /**
    * 当有线程调用该方法时,唤醒等待队列中的一个线程节点
    * 并将该线程从等待队列移动同步队列阻塞等待锁资源获取
    * signal()相当于synchronized等待唤醒机制中的notify()方法
    */
    void signal();
    
    /**
    * 作用与signal()相同,不过该方法的作用是唤醒该等待队列中的所有线程节点
    * signalAll()相当于synchronized等待唤醒机制中的notifyAll()方法
    */
    void signalAll();
}

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

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

相关文章

【2024年华数杯C题老外游中国】(完整题解+代码+完整参考论文)

请问 352 个城市中所有 35200 个景点评分的最高分&#xff08;Best Score&#xff0c;简称 BS&#xff09;是多少&#xff1f;全国有多少个景点获评了这个最高评分&#xff08;BS&#xff09;&#xff1f;获评了这个最高评分&#xff08;BS&#xff09;景点最多的城市有哪些&am…

2024带你轻松玩转Parallels Desktop19虚拟机!让你在Mac电脑上运行Windows系统

大家好&#xff0c;今天我要给大家安利一款神奇的软件——Parallels Desktop 19虚拟机。这款软件不仅可以让你在Mac电脑上运行Windows系统&#xff0c;还能轻松切换两个操作系统之间的文件和应用程序&#xff0c;让你的工作效率翻倍&#xff01; 让我来介绍一下Parallels Desk…

【口语】基础英语之疑问句 | 描述一个认为音乐很重要的人

文章目录 一、基础英语之疑问句二、口语题&#xff1a;描述一个认为音乐很重要并且喜欢音乐的人 一、基础英语之疑问句 英语中的疑问句可以根据结构和用途被分为几种主要类型&#xff1a; 一般疑问句&#xff08;General Questions&#xff09;: 结构&#xff1a;助动词 主语…

Learn ComputeShader 03 Passing data to shader

这次我们想要在一个平面中生成随机运动的圆形。之前传递数据都是通过setInt&#xff0c;setVector等方法进行的&#xff0c;但是这些方法并不能一下传递大量数据&#xff0c;比如一个结构体数组&#xff0c;一个数据块。所以这次的主要内容就是通过buffer传递大量数据。 首先是…

Android 本地化、多语言切换:Localization

目录 1&#xff09;如何实现多语言切换、如何实现跟随手机语言切换而切换app语言 2&#xff09;Localization是什么 3&#xff09;不管手机语言如何&#xff0c;根据用户在App选择的语言&#xff0c;只切换App语言 4&#xff09;文字长短不一样&#xff0c;怎么办呢? 一、Lo…

积分的简介

积分的简介 集成是一种添加切片以找到整体的方法。积分可用于查找区域、体积、中心点和许多有用的东西。但是&#xff0c;最简单的方法是从找到函数和 x 轴之间的区域开始&#xff0c;如下所示&#xff1a; 1.面积是什么&#xff1f;是片 我们可以在几个点上计算函数&#xf…

Error in importing environment OpenAI Gym

题意&#xff1a;尝试导入OpenAI Gym库中的某个环境时发生了错误 问题背景&#xff1a; I am trying to run an OpenAI Gym environment however I get the following error: 我正在尝试运行一个OpenAI Gym环境&#xff0c;但是我遇到了以下错误&#xff1a; import gym env…

Spring Boot整合MyBatis-Flex

说明&#xff1a;MyBatis-Flex&#xff08;官网地址&#xff1a;https://mybatis-flex.com/&#xff09;&#xff0c;是一款数据访问层框架&#xff0c;可实现项目中对数据库的访问&#xff0c;类比MyBatis-Plus。本文介绍&#xff0c;在Spring Boot项目整合MyBatis-Flex。 创…

专业解析:U盘打不开的应对与数据恢复策略

一、U盘打不开的困境解析 在日常的数据存储与传输中&#xff0c;U盘作为便携的存储媒介&#xff0c;其重要性不言而喻。然而&#xff0c;当您急需使用U盘时&#xff0c;却遭遇“U盘打不开”的尴尬境地&#xff0c;这无疑会给工作和学习带来极大的不便。U盘打不开的原因多种多样…

Javase--Date

1.Date简介 Date的学习: 1. java.util包下的类 2.用于日期、时间的描述 3. 实际上时距离一个固定时间点1970年1月1日00:00:00的毫秒数 4.我们常用的是格林威治时间:GMT UTC:世界调整时间 5.固定时间点:说的其实是本初子午线的时间。因此北京时间是1970年1月1日8:00:…

评估生成分子/对接分子的物理合理性工具 PoseBusters 评测

最近在一些分子生成或者对接模型中&#xff0c;出现了新的评估方法 PoseBusters&#xff0c;用于评估生成的分子或者对接的分子是否符合化学有效性和物理合理性。以往的分子生成&#xff0c;经常以生成分子的有效性、新颖性、化学空间分布&#xff0c;与口袋的结合力等方面进行…

.NET反混淆神器de4dot使用介绍

最近在逛看雪时&#xff0c;发现一个帖子&#xff0c;[原创]常见语言基础逆向方法合集-软件逆向-看雪-安全社区|安全招聘|kanxue.com。里面介绍 了常见语言基础逆向方法合集。关于.net程序逆向这块&#xff0c;介绍了三个工具。 .NET Reflector .NET Decompiler: Decompile A…

C++中string类常用函数的用法介绍

在C中&#xff0c;string是一个功能强大的类&#xff0c;用于处理和操作文本数据。它属于C标准库中的字符串库部分&#xff0c;专门用于处理字符串。与传统的C风格字符串相比&#xff0c;它提供了动态内存管理、类型安全和丰富的操作方法。 目录 一、构造和初始化 二、获取字…

算法训练,项目

一.木材加工 题解&#xff1a; 二分答案&#xff0c;左边0&#xff0c;右边可以为最长的木头&#xff0c;但我直接赋值了一个很大的值&#xff0c;进行二分&#xff0c;随后写个check;内部遍历木头截取为mid木块的个数&#xff0c;要是>k&#xff0c;满足要求&#xff0c;还…

【时时三省】(C语言基础)一维数组

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ——csdn时时三省 数组 数组就是一组数 数组的官方定义是一组相同类型元素的集合 一堆数组的创建和初始化 求组的创建 数组是一组相同类型元素的集合。数组的创建当时是: type&#xff3f;t arr&#x…

【JavaEE】定时器

目录 前言 什么是定时器 如何使用java中的定时器 实现计时器 实现MyTimeTask类 Time类中存储任务的数据结构 实现Timer中的schedule方法 实现MyTimer中的构造方法 处理构造方法中出现的线程安全问题 完整代码 考虑在限时等待wait中能否用sleep替换 能否用PriorityBlo…

Linux网络——深入理解 epoll

目录 一、epoll 模型 1.1 前导知识 1.1.1 宏 offsetof 1.1.2 手动计算 1.2 epoll 模型 二、 epoll 工作模式 2.1 水平触发 特点&#xff1a; 2.2 边缘触发 特点&#xff1a; 边缘触发模式中的循环读取 结合非阻塞模式的优势 一、epoll 模型 经过了之前的学习&#…

什么是容器查询?分享 1 段优质 CSS 代码片段!

本内容首发于工粽号&#xff1a;程序员大澈&#xff0c;每日分享一段优质代码片段&#xff0c;欢迎关注和投稿&#xff01; 大家好&#xff0c;我是大澈&#xff01; 本文约 700 字&#xff0c;整篇阅读约需 1 分钟。 今天分享一段优质 CSS 代码片段&#xff0c;使用容器查询…

【算法设计题】实现以字符串形式输入的简单表达式求值,第2题(C/C++)

目录 第2题 实现以字符串形式输入的简单表达式求值 得分点&#xff08;必背&#xff09; 题解 1. 初始化和变量定义 2. 获取第一个数字并存入队列 3. 遍历表达式字符串&#xff0c;处理运算符和数字 4. 初始化 count 并处理加减法运算 代码详解 &#x1f308; 嗨&#xf…

你还在为PDF文件烦恼吗?试试这四款合并工具吧!

每天应对工作都是一个头两个大的&#xff0c;其中pdf的文件问题就是恼人的工作量之一了&#xff0c;这几年的工作经历下来也找了各种可以帮助解决PDF文件问题的工具&#xff0c;好在使用了一些助力我高效工作的软件&#xff0c;今天针对其中遇到的解决pdf合并问题的四款宝藏工具…