目录

AQS的源码分析 <二>

该篇主要分析AQS的ConditionObject,是AQS的内部类,实现等待通知机制。

1、条件队列

条件队列与AQS中的同步队列有所不同,结构图如下:

两者区别:

  • 1、链表结构不同,条件队列是单向链表,同步队列是双向链表。
  • 2、两个队列中等待条件不同,条件队列中线程是已经获取到锁,主动调用await方法释放锁,挂起当前线程,等待某个条件(如IO,mq消息等),同步队列中的线程是等待获取锁,在获取锁失败后挂起等待锁可用。

两者联系:

当等待的某个条件完成,其他线程调用signal方法,通知挂起在条件队列中的线程,会将条件队列中该node移出,加入到同步队列中,node的ws状态由Node.CONDITION改为0 ,开始等待锁。

2、ConditionObject

ConditionObject 和 Node一样,都是AQS的内部类, ConditionObject实现Condition接口,主要实现线程调用 await和signal ,实现线程条件阻塞和通知机制,Condition对象通过 Lock子类调用newConditon方法获取,以

ReentrantLock为例,代码如下:

ReentrantLock lock  = new ReentrantLock();
Condition condition = lock.newCondition();

可见排他锁的newCondition方法返回的是ConditionObject对象

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

简单生产者消费示例代码:

package AQS;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zdd
* 2019/12/30 下午
* Description: 利用ReentrantLock和Condition实现生产者消费者
*/
public class ConditionTest {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
//资源类
Apple apple = new Apple();
//1.开启生产者线程
new Thread(()-> {
for (;;) {
lock.lock();
try {
//苹果没有被消费,吃完通知我,我再生产哦
if (apple.getNumber() > 0) {
condition.await();
}
TimeUnit.SECONDS.sleep(1);
System.out.println("生产一个苹果");
apple.addNumber();
//通知消费线程消费
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
},"producer").start();
//2.开启消费者线程
new Thread(()-> {
for (;;) {
lock.lock();
try {
//苹果数量为0,挂起等待生产苹果,有苹果了会通知
if(apple.getNumber() == 0) {
condition.await();
}
TimeUnit.SECONDS.sleep(1);
System.out.println("消费一个苹果");
apple.decreNumber();
//通知生产线程生产
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
},"consumer").start(); }
//定义苹果内部类
static class Apple {
//记录苹果数量
private Integer number =0;
public void addNumber() {
number++;
System.out.println(Thread.currentThread().getName() +"当前苹果数量:"+number );
}
public void decreNumber() {
number--;
System.out.println(Thread.currentThread().getName() +"当前苹果数量:"+number);
}
public Integer getNumber() {
return number;
}
}
}

执行结果如下图:

2.1、 await() 方法

当前线程是在已经获取锁的情况下,调用await方法主动释放锁,挂起当前线程,等待某个条件(IO,mq消息等)唤醒,再去竞争获取锁的过程。该方法会将当前线程封装到node节点中,添加到Condition条件队列中,释放锁资源,并挂起当前线程。

具体执行步骤如下:

1、线程封装到node中,并添加到Condition条件队列中,ws =-2 即为Node.CONDITION。

2、释放锁。

3、将自己阻塞挂起,如果线程被唤醒,首先检查自己是被中断唤醒的不。如果是被中断唤醒,跳出while循环;如果是被其他线程signal唤醒,则判断当前线程所在node是否被加入到同步等待队列,已在同步队列中也跳出while循环,否则继续挂起,signal唤醒逻辑会将condition条件队列node 移出,加入到同步队列中,去等待获取锁。

4,线程被唤醒,执行acquireQueued方法,线程会尝试获取锁,若失败则在同步队列中找到安全位置阻塞,成功则从调用await()方法处继续向下执行,返回值表示当前线程是否被中断过。

public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//挂起当前线程
LockSupport.park(this);
// 被唤醒: 1,被其他线程唤醒,2,中断唤醒,
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//1,如果被signal正常唤醒执行acquireQueued,返回false,如果获取到锁就继续执行调用await后面的代码了,未获取到锁就在同步队列中继续挂起等待锁执行了
//2,如果被中断唤醒的,acquireQueued 返回true
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
//线程在被signal后,再被中断的
interruptMode = REINTERRUPT;
// 后面代码处理的是被中断唤醒的情况
if (node.nextWaiter != null) // clean up if cancelled
//如果nextWaiter!=null,则表示还在条件队列中,清理一下所有被取消node
//什么情况下会进入该if判断中,如果是正常被signal的,会将该node从条件队列移出加入到同步队列中的, nextWaiter 一定为null,那就是被异常中断情况,
unlinkCancelledWaiters();
if (interruptMode != 0)
//响应中断模式
reportInterruptAfterWait(interruptMode);
}

第1步,执行addConditionWaiter方法,主要逻辑是将线程封装为Node,并添加到条件队列中

        private Node addConditionWaiter() {
//1.获取队列中最后一个节点
Node t = lastWaiter;
//2.如果最后一个节点被取消,清除出队
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//3. t 指向最新有效的节点,也可能条件队列为空,t==null
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}

第2步,完全释放锁 fullyRelease,将同步状态state 设置为初始值0,这里考虑到有多次重入获取锁情况,state >1,这时需完全释放锁。

  final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
//1,释放锁
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
//2,释放锁失败,将条件队列中的节点标记为取消
node.waitStatus = Node.CANCELLED;
}
}

isOnSyncQueue 判断node是否在同步队列中

 final boolean isOnSyncQueue(Node node) {
//1,这2种情况肯定没有在同步队列中
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
//3.从同步队列尾节点开始对比,看是否在同步队列中
return findNodeFromTail(node);
}

findNodeFromTail 从后向前寻找

  private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}

在线程被唤醒后,检查挂起期间是否被中断

private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}

如果线程被中断了,那就需要将在条件队列中等待的该节点执行 transferAfterCancelledWait

 final boolean transferAfterCancelledWait(Node node) {
// 判断是否是被signal通知唤醒的,会更新为0,更新成功,执行入队操作(加入同步队列)
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
while (!isOnSyncQueue(node))
//未在同步队列中,让出处理器,线程回到就绪态,等待下一次分配cpu调度
Thread.yield();
return false;
}

最后根据不同的中断值做出相应处理

private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
//1,直接抛出中断异常
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
//2,中断标志
selfInterrupt();
}

2.2、signal方法

就是将条件队列中的node移出,加入到同步队列等待获取锁的过程。

流程图如下:

  public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
   private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 1、将first节点执行出队操作
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
//2,如果条件队列中有ws =-2的节点,肯定会移出一个到同步队列中
}
final boolean transferForSignal(Node node) {
//1,将node ws更新为0 ,如果node 状态不等于CONDITION,一定是被取消了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//2,加入到同步队列中,返回的p是node的pre
Node p = enq(node);
int ws = p.waitStatus;
//3,如果前置节点被取消,或者更新p的 ws =-1 失败,直接唤醒线程,否则等待前置节点唤醒自己
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//唤醒线程
LockSupport.unpark(node.thread);
return true;
}

3、总结

1、Condition提供的阻塞通知机制与Object类两者对比:

  • 方法不同,Condition提供方法有 await(), signal(),signalAll(), Object类提供的是wait(),notify() , notifyAll()
  • 配合使用对象不同,Condition条件需要和Lock配合使用,Object类需和Synchronized关键字配合。
  • 多条件, Condition可实现多个条件,即创建多个Condition对象,可以每个Condition对象对应一种条件,从而有选择的实现唤醒通知,Object类的要唤醒一个阻塞线程,只能在一个条件队列中,唤醒是随机的,没有Condition使用灵活。

2、注意区别Condition条件队列与同步队列两者的区别,2个队列中线程等待条件不同

多线程之美8一 AbstractQueuedSynchronizer源码分析<二>的更多相关文章

  1. 多线程之美5一 AbstractQueuedSynchronizer源码分析<一>

    AQS的源码分析 目录结构 1.什么是CAS ? 2.同步器类结构 3.CLH同步队列 4.AQS中静态内部类Node 5.方法分析 ​ 5.1.acquire(int arg ) ​ 5.2.rel ...

  2. 多线程之美7一ReentrantReadWriteLock源码分析

    目录 前言 在多线程环境下,为了保证线程安全, 我们通常会对共享资源加锁操作,我们常用Synchronized关键字或者ReentrantLock 来实现,这两者加锁方式都是排他锁,即同一时刻最多允许 ...

  3. AbstractQueuedSynchronizer源码分析

    AbstractQueuedSynchronizer源码分析 前提 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)是并发编程大师D ...

  4. Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...

  5. Java并发系列[3]----AbstractQueuedSynchronizer源码分析之共享模式

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快 ...

  6. Java并发系列[4]----AbstractQueuedSynchronizer源码分析之条件队列

    通过前面三篇的分析,我们深入了解了AbstractQueuedSynchronizer的内部结构和一些设计理念,知道了AbstractQueuedSynchronizer内部维护了一个同步状态和两个排 ...

  7. Fresco 源码分析(二) Fresco客户端与服务端交互(1) 解决遗留的Q1问题

    4.2 Fresco客户端与服务端的交互(一) 解决Q1问题 从这篇博客开始,我们开始讨论客户端与服务端是如何交互的,这个交互的入口,我们从Q1问题入手(博客按照这样的问题入手,是因为当时我也是从这里 ...

  8. 框架-springmvc源码分析(二)

    框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...

  9. Tomcat源码分析二:先看看Tomcat的整体架构

    Tomcat源码分析二:先看看Tomcat的整体架构 Tomcat架构图 我们先来看一张比较经典的Tomcat架构图: 从这张图中,我们可以看出Tomcat中含有Server.Service.Conn ...

随机推荐

  1. 域名拆分 tld

    概念 URL Universal Resource Locator ,统一资源定位符. 用处:用来标识互联网资源的唯一地址. 本质:提供了互联网上任一资源地址的通用表示方法. protocol://h ...

  2. LightOJ 1341 Aladdin and the Flying Carpet【整数分解】

    题目链接: http://lightoj.com/login_main.php?url=volume_showproblem.php?problem=1341 题意: 给定一个数,将其拆分成两个数的乘 ...

  3. @loj - 2288@「THUWC 2017」大葱的神力

    目录 @description@ @solution@ @data - 1@ @data - 2@ @data - 3@ @data - 4@ @data - 5@ @data - 6@ @data ...

  4. Eclipsed的SVN插件不能识别之前工作空间的项目

    今天用Eclipse的SVN插件,由于之前项目是用"tortoisesvn"进行checkout的,并且已经在工作空间中使用.后来用来Ecplise的SVN插件,可是发现右键Tea ...

  5. C++ sort使用两个参数来排序

    排序在编程中经常用到,冒泡法排序时间复杂度高,使用C++库函数sort可以快速排序. 1.必须的头文件#include < algorithm>和using namespace std;  ...

  6. <STL源码剖析> 6.3.6 power

    计算power的算法说明 http://www.sxt.cn/u/324/blog/2112 翻译自  http://videlalvaro.github.io/2014/03/the-power-a ...

  7. 五分钟搭建一个基于BERT的NER模型

    BERT 简介 BERT是2018年google 提出来的预训练的语言模型,并且它打破很多NLP领域的任务记录,其提出在nlp的领域具有重要意义.预训练的(pre-train)的语言模型通过无监督的学 ...

  8. 2019-10-5-dotnet-core-获取-MacAddress-地址方法

    title author date CreateTime categories dotnet core 获取 MacAddress 地址方法 lindexi 2019-10-05 10:44:10 + ...

  9. Python--day26--面向对象思维导图

  10. Codeforces Round #188 (Div. 1 + Div. 2)

    A. Even Odds 奇数个数\(\lfloor \frac{n+1}{2}\rfloor\) B. Strings of Power 从位置0开始,统计heavy个数,若当前为metal,则可以 ...