在使用Lock之前,我们使用的最多的同步方式应该是synchronized关键字来实现同步方式了。配合Object的wait()、notify()系列方法可以实现等待/通知模式。

Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。Object和Condition接口的一些对比。摘自《Java并发编程的艺术》

一、我们首先来看一下整个Condition的运行机制及其java中Condition接口有哪些方法

当我们调用lock.newCondition()方法时,就会创建出一个等待队列(一个lock可以对应多个等待队列)。等我们调用condition的await()方法时,就会把当前的Node节点从阻塞队列中移到等待队列的末尾。

当使用singal()方法时,我们从对应等待队列的队头取出一个Node并且将其放入到阻塞队列中,如果是singalAll()方法,则把对应等待队列中所有的Node节点都移到阻塞队列中。

java中Condition接口中具体方法如下

二、AQS中ConditionObject的await方法分析

await()方法有多种变种,包括不响应中断的await,带有中断时间的await等等,但是基本思想都与await()方法相似,本文只分析await()方法。await()方法的分析如下


public final void await() throws InterruptedException {
//If current thread is interrupted, throw InterruptedException.
if (Thread.interrupted())
throw new InterruptedException();

//将调用await()方法的线程封装为一个node节点加入到条件队列中,并且返回该节点的一个引用
Node node = addConditionWaiter();
//完全释放掉当前线程对应的锁(将state的值置为0)
  //为什么要释放锁呢?  加着锁 挂起后,谁还能救你呢?
int savedState = fullyRelease(node);
//0 在condition队列挂起期间未接收过过中断信号
//-1 在condition队列挂起期间接收到中断信号了
//1 在condition队列挂起期间未接收到中断信号,但是迁移到“阻塞队列”之后 接收过中断信号。
int interruptMode = 0;

//isOnSyncQueue
//true:表示当前线程对应的node节点已经迁移到阻塞队列了
//false说明当前node仍然还在条件队列中,需要继续park!
while (!isOnSyncQueue(node)) {
//挂起当前线程,等待其他线程的唤醒
LockSupport.park(this);
//什么时候会被唤醒?都有几种情况呢?
//1.常规路径:外部线程获取到lock之后,调用了 signal()方法 转移条件队列的头节点到 阻塞队列, 当这个节点获取到锁后,会唤醒。
//2.转移至阻塞队列后,发现阻塞队列中的前驱节点状态 是 取消状态,此时会唤醒当前节点
//3.当前节点挂起期间,被外部线程使用中断唤醒..

//checkInterruptWhileWaiting :就算在condition队列挂起期间 线程发生中断了,对应的node也会被迁移到 “阻塞队列”。
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//执行到这里,就说明 当前node已经迁移到 “阻塞队列”了

//acquireQueued :竞争队列的逻辑..
//条件一:返回true 表示在阻塞队列中 被外部线程中断唤醒过..
//条件二:interruptMode != THROW_IE 成立,说明当前node在条件队列内 未发生过中断
//设置interruptMode = REINTERRUPT
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//考虑下 node.nextWaiter != null 条件什么时候成立呢?
//其实是node在条件队列内时 如果被外部线程 中断唤醒时,会加入到阻塞队列,但是并未设置nextWaiter = null。
if (node.nextWaiter != null) // clean up if cancelled
//清理条件队列内取消状态的节点..
unlinkCancelledWaiters();

//条件成立:说明挂起期间 发生过中断(1.条件队列内的挂起 2.条件队列之外的挂起)
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}

  

/**
* 判断在等待过程中该节点是否被中断过
*/
private int checkInterruptWhileWaiting(Node node) {
//Thread.interrupted() 返回当前线程中断标记位,并且重置当前标记位 为 false 。
return Thread.interrupted() ?
//transferAfterCancelledWait 这个方法只有在线程是被中断唤醒时 才会调用!
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
/**
* Transfers node, if necessary, to sync queue after a cancelled wait.
* Returns true if thread was cancelled before being signalled.
*
* @param node the node
* @return true if cancelled before the node was signalled
*/
final boolean transferAfterCancelledWait(Node node) {
//条件成立:说明当前node一定是在 条件队列内,因为signal 迁移节点到阻塞队列时,会将节点的状态修改为0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//中断唤醒的node也会被加入到 阻塞队列中!!
enq(node);
//true:表示是在条件队列内被中断的.
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
//执行到这里有几种情况?
//1.当前node已经被外部线程调用 signal 方法将其迁移到 阻塞队列内了。
//2.当前node正在被外部线程调用 signal 方法将其迁移至 阻塞队列中 进行中状态..

while (!isOnSyncQueue(node))
Thread.yield();
//false:表示当前节点被中断唤醒时 不在 条件队列了..
return false;
}
/**
* 将处于取消状态的节点从等待队列中移除
*/
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
//从头开始遍历
while (t != null) {
Node next = t.nextWaiter;
//条件成立:说明当前节点状态为 取消状态
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
 /**
* Throws InterruptedException, reinterrupts current thread, or
* does nothing, depending on mode.
*/
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}

三、AQS中ConditionObject的addConditionWaiter方法分析




/**
* 将当前线程封装为一个Node加入等待队列中
* @return 返回封装的Node节点
*
* 流程
* 1、判断当前等待队列的尾节点否为取消状态(t != null && t.waitStatus != Node.CONDITION)
* 将处于取消状态的节点从等待队列中移除
* 2、将当前的线程封装为一个Node,并将其加入到等待队列中
*/
private Node addConditionWaiter() {
//获取当前条件队列的尾节点的引用 保存到局部变量 t中
Node t = lastWaiter;

// If lastWaiter is cancelled, clean out.
//条件一:t != null 成立:说明当前条件队列中,已经有node元素了..
//条件二:node 在 条件队列中时,它的状态是 CONDITION(-2)
// t.waitStatus != Node.CONDITION 成立:说明当前node已经被取消了..
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 = node;
return node;
}

四、AQS中ConditionObject的fullyRelease方法分析


/**
* 用当前状态值调用释放,返回state的值
*/
final int fullyRelease(Node node) {
//完全释放锁是否成功,当failed失败时,说明当前线程是未持有锁调用 await方法的线程..(错误写法..)
//假设失败,在finally代码块中 会将刚刚加入到 条件队列的 当前线程对应的node状态 修改为 取消状态
//后继线程就会将 取消状态的 节点 给清理出去了..
boolean failed = true;
try {
//获取当前线程 所持有的 state值 总数!
int savedState = getState();

//绝大部分情况下:release 这里会返回true。
if (release(savedState)) {
//失败标记设置为false
failed = false;
//返回当前线程释放的state值
//为什么要返回savedState?
//因为在当你被迁移到“阻塞队列”后,再次被唤醒,且当前node在阻塞队列中是head.next 而且
//当前lock状态是state == 0 的情况下,当前node可以获取到锁,此时需要将state 设置为savedState.
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}

public final boolean release(int arg) {
//head什么情况下会被创建出来?
//当持锁线程未释放线程时,且持锁期间 有其它线程想要获取锁时,其它线程发现获取不了锁,而且队列是空队列,此时后续线程会为当前持锁中的
//线程 构建出来一个head节点,然后后续线程 会追加到 head 节点后面。
if (tryRelease(arg)) {
Node h = head;
//条件一:成立,说明队列中的head节点已经初始化过了,ReentrantLock 在使用期间 发生过 多线程竞争了...
//条件二:条件成立,说明当前head后面一定插入过node节点。
if (h != null && h.waitStatus != 0)
//唤醒后继节点..
unparkSuccessor(h);
return true;
}
return false;}

五、AQS中ConditionObject的isOnSyncQueue方法分析


final boolean isOnSyncQueue(Node node) {
//条件一:node.waitStatus == Node.CONDITION 条件成立,说明当前node一定是在
//条件队列,因为signal方法迁移节点到 阻塞队列前,会将node的状态设置为 0
//条件二:前置条件:node.waitStatus != Node.CONDITION ===>
// 1.node.waitStatus == 0 (表示当前节点已经被signal了)
// 2.node.waitStatus == 1 (当前线程是未持有锁调用await方法..最终会将node的状态修改为 取消状态..)
//node.waitStatus == 0 为什么还要判断 node.prev == null?
//因为signal方法 是先修改状态,再迁移。
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//条件满足,说明其一定在等待队列中
if (node.next != null) // If has successor, it must be on queue
return true;
/*
* node.prev can be non-null, but not yet on queue because
* the CAS to place it on queue can fail. So we have to
* traverse from tail to make sure it actually made it. It
* will always be near the tail in calls to this method, and
* unless the CAS failed (which is unlikely), it will be
* there, so we hardly ever traverse much.
*/
/**
* 执行到这里,说明当前节点的状态为:node.prev != null 且 node.waitStatus == 0
* findNodeFromTail 从阻塞队列的尾巴开始向前遍历查找node,如果查找到 返回true,查找不到返回false
* 当前node有可能正在signal过程中,正在迁移中...还未完成...
*/
return findNodeFromTail(node);
}
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
 

六、AQS中singal的逻辑分析(singalAll逻辑相同)

public final void signal() {
//判断调用signal方法的线程是否是独占锁持有线程,如果不是,直接抛出异常..
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取条件队列的一个node
Node first = firstWaiter;
//第一个节点不为null,则将第一个节点 进行迁移到 阻塞队列的逻辑..
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
//firstWaiter = first.nextWaiter 因为当前first马上要出条件队列了,
//所以更新firstWaiter为 当前节点的下一个节点..
//如果当前节点的下一个节点 是 null,说明条件队列只有当前一个节点了...当前出队后,整个队列就空了..
//所以需要更新lastWaiter = null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//当前first节点 出 条件队列。断开和下一个节点的关系.
first.nextWaiter = null;
//transferForSignal(first)
//boolean:true 当前first节点迁移到阻塞队列成功 false 迁移失败...
//while循环 :(first = firstWaiter) != null 当前first迁移失败,则将first更新为 first.next 继续尝试迁移..
//直至迁移某个节点成功,或者 条件队列为null为止。
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
//cas修改当前节点的状态,修改为0,因为当前节点马上要迁移到 阻塞队列了
//成功:当前节点在条件队列中状态正常。
//失败:1.取消状态 (线程await时 未持有锁,最终线程对应的node会设置为 取消状态)
// 2.node对应的线程 挂起期间,被其它线程使用 中断信号 唤醒过...(就会主队进入到 阻塞队列,这时也会修改状态为0)
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;

/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
//enq最终会将当前 node 入队到 阻塞队列,p 是当前节点在阻塞队列的 前驱节点.
Node p = enq(node);
//条件一:ws > 0 成立:说明前驱节点的状态在阻塞队列中是 取消状态,唤醒当前节点。
//条件二:前置条件(ws <= 0),
//compareAndSetWaitStatus(p, ws, Node.SIGNAL) 返回true 表示设置前驱节点状态为 SIGNAl状态成功
//compareAndSetWaitStatus(p, ws, Node.SIGNAL) 返回false ===> 什么时候会false?
//当前驱node对应的线程 是 lockInterrupt入队的node时,是会响应中断的,外部线程给前驱线程中断信号之后,前驱node会将
//状态修改为 取消状态,并且执行 出队逻辑..
//前驱节点状态 只要不是 0 或者 -1 那么,就唤醒当前线程。
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}

juc下Condition类解析的更多相关文章

  1. JUC下工具类CountDownLatch用法以及源码理解

    CountDownLoatch是JUC下一个用于控制计数的计数器,比如我需要从6开始计数,每个线成运行完之后计数减一,等计数器到0时候开始执行其他任务. public static void main ...

  2. Java 经典面试题:聊一聊 JUC 下的 LinkedBlockingQueue

    本文聊一下 JUC 下的 LinkedBlockingQueue 队列,先说说 LinkedBlockingQueue 队列的特点,然后再从源码的角度聊一聊 LinkedBlockingQueue 的 ...

  3. sqlalchemy mark-deleted 和 python 多继承下的方法解析顺序 MRO

    sqlalchemy mark-deleted 和 python 多继承下的方法解析顺序 MRO 今天在弄一个 sqlalchemy 的数据库基类的时候,遇到了跟多继承相关的一个小问题,因此顺便看了一 ...

  4. 【Owin 学习系列】2. Owin Startup 类解析

    Owin Startup 类解析 每个 Owin 程序都有 startup 类,在这个 startup 类里面你可以指定应用程序管道模型中的组件.你可以通过不同的方式来连接你的 startup 类和运 ...

  5. SpringBoot入门(三)——入口类解析

    本文来自网易云社区 上一篇介绍了起步依赖,这篇我们先来看下SpringBoot项目是如何启动的. 入口类 再次观察工程的Maven配置文件,可以看到工程的默认打包方式是jar格式的. <pack ...

  6. java多线程,多线程加锁以及Condition类的使用

    看了网上非常多的运行代码,很多都是重复的再说一件事,可能对于java老鸟来说,理解java的多线程是非常容易的事情,但是对于我这样的菜鸟来说,这个实在有点难,可能是我太菜了,网上重复的陈述对于我理解这 ...

  7. JDK源码之String类解析

    一 概述 String由final修饰,是不可变类,即String对象也是不可变对象.这意味着当修改一个String对象的内容时,JVM不会改变原来的对象,而是生成一个新的String对象 主要考虑以 ...

  8. java.io 包下的类有哪些 + 面试题

    java.io 包下的类有哪些 + 面试题 IO 介绍 IO 是 Input/Output 的缩写,它是基于流模型实现的,比如操作文件时使用输入流和输出流来写入和读取文件等. IO 分类 传统的 IO ...

  9. Spark 资源调度包 stage 类解析

    spark 资源调度包 Stage(阶段) 类解析 Stage 概念 Spark 任务会根据 RDD 之间的依赖关系, 形成一个DAG有向无环图, DAG会被提交给DAGScheduler, DAGS ...

随机推荐

  1. Android Studio 4.x

    Android Studio 4.x https://developer.android.com/studio https://d.android.com/r/studio-ui/whats-new- ...

  2. Ethical Hacking Tutorials

    Ethical Hacking Tutorials Free Ethical Hacking Tutorials https://www.guru99.com/ethical-hacking-tuto ...

  3. 用Qt写了个将视频设置为壁纸的软件

    软件功能很简单,使用时占用的资源和播放的视频有关: 依赖于FFplay,Github源码 效果图:

  4. 2021-2-19:请问你知道 Java 如何高性能操作文件么?

    一般高性能的涉及到存储框架,例如 RocketMQ,Kafka 这种消息队列,存储日志的时候,都是通过 Java File MMAP 实现的,那么什么是 Java File MMAP 呢? 什么是 J ...

  5. django中间件介绍

    在学习django中间件之前,先来认识一下django的生命周期,如下图所示: django生命周期:浏览器发送的请求会先经过wsgiref模块处理解析出request(请求数据)给到中间件,然后通过 ...

  6. django学习-11.开发一个简单的醉得意菜单和人均支付金额查询页面

    1.前言 刚好最近跟技术部门的[产品人员+UI人员+测试人员],组成了一桌可以去公司楼下醉得意餐厅吃饭的小team. 所以为了实现这些主要点餐功能: 提高每天中午点餐效率,把点餐时间由20分钟优化为1 ...

  7. C++算法代码——卡片游戏

    题目来自:http://218.5.5.242:9018/JudgeOnline/problem.php?cid=1397&pid=2 题目描述 桌上有一叠牌,从第一张牌(即位于顶面的牌)开始 ...

  8. 一些小Tip

    导语 个人感悟,持续更新中... 正文 无论NIO还是AIO,都没有在数据传输过程(tcp/udp)作革命性的创新.他们在传输过程的效率和传统BIO是一样的,还是会产生阻塞(网络延迟,Socket缓冲 ...

  9. 生成类库项目时同时生成的pdb文件是什么东东?

    英文全称:Program Database File Debug里的PDB是full,保存着调试和项目状态信息.有断言.堆栈检查等代码.可以对程序的调试配置进行增量链接.Release 里的PDB是p ...

  10. tomcat运行多个项目同一个端口与不同端口的设置

    一.首先打包项目 这里采用eclipse开发工具,选中项目右击,点击Export进入 选择web下的 WAR file ,点击next 在这里可能有坑,新装的eclipse没有web文件夹 此时需要下 ...