17.AQS中的Condition是什么?

这篇具有很好参考价值的文章主要介绍了17.AQS中的Condition是什么?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

欢迎关注:王有志
期待你加入Java人的提桶跑路群:共同富裕的Java人

今天来和大家聊聊ConditionCondition为AQS“家族”提供了等待与唤醒的能力,使AQS"家族"具备了像synchronized一样暂停与唤醒线程的能力。我们先来看两道关于Condition的面试题目:

  • ConditionObject的等待与唤醒有什么区别?
  • 什么是Condition队列?

接下来,我们就按照“是什么”,“怎么用”和“如何实现”的顺序来揭开Condition的面纱吧。

Condition是什么?

Condition是Java中的接口,提供了与Object#waitObject#notify相同的功能。Doug Lea在Condition接口的描述中提到了这点:

Conditions (also known as condition queues or condition variables) provide a means for one thread to suspend execution (to "wait") until notified by another thread that some state condition may now be true.

来看Condition接口中提供了哪些方法:

public interface Condition {
  void await() throws InterruptedException;
  
  void awaitUninterruptibly();
  
  long awaitNanos(long nanosTimeout) throws InterruptedException;
  
  boolean await(long time, TimeUnit unit) throws InterruptedException;
  
  boolean awaitUntil(Date deadline) throws InterruptedException;
  
  void signal();
  
  void signalAll();
}

Condition只提供了两个功能:等待(await)和唤醒(signal),与Object提供的等待与唤醒时相似的:

public final void wait() throws InterruptedException;
  
public final void wait(long timeoutMillis, int nanos) throws InterruptedException;

public final native void wait(long timeoutMillis) throws InterruptedException;

@HotSpotIntrinsicCandidate
public final native void notify();

@HotSpotIntrinsicCandidate
public final native void notifyAll();

唤醒功能上,ConditionObject的差异并不大:

  • Condition#signal\(\approx\)Object#notify

  • Condition#signalAll\(=\)Object#notifyAll

多个线程处于等待状态时,Object#notify()是“随机”唤醒线程,而Condition#signal则由具体实现决定如何唤醒线程,如:ConditionObject唤醒的是最早进入等待的线程但两个方法均只唤醒一个线程。

等待功能上,ConditionObject的共同点是:都会释放持有的资源Condition释放锁Object释放Monitor,即进入等待状态后允许其他线程获取锁/监视器。主要的差异体现在Condition支持了更加丰富的场景,通过一张表格来对比下:

Condition方法 Object方法 解释
Condition#await() Object#wait() 暂停线程,抛出线程中断异常
Condition#awaitUninterruptibly() / 暂停线程,不抛出线程中断异常
Condition#await(time, unit) Object#wait(timeoutMillis, nanos) 暂停线程,直到被唤醒或等待指定时间后,超时后自动唤醒返回false,否则返回true
Condition#awaitUntil(deadline) / 暂停线程,直到被唤醒或到达指定时间点,超时后自动唤醒返回false,否则返回true
Condition#awaitNanos(nanosTimeout) / 暂停线程,直到被唤醒或等待指定时间后,返回值表示被唤醒时的剩余时间(nanosTimeout-耗时),结果为负数表示超时

除了以上差异外,Condition还支持创建多个等待队列,即同一把锁拥有多个等待队列,线程在不同队列中等待,而Object只有一个等待队列。《Java并发编程的艺术》中也有一张类似的表格,放在这里供大家参考:
17.AQS中的Condition是什么?

Tips

  • 实际上signal翻译为唤醒并不恰当~~
  • 涉及到Condition的实现部分,下文通过AQS中的ConditionObject详细解释。

Condition怎么用?

既然ConditionObject提供的等待与唤醒功能相同,那么它们的用法是不是也很相似呢?

与调用Object#waitObject#notifyAll必须处于synchronized修饰的代码中一样(获取Monitor),调用Condition#awaitCondition#signalAll的前提是要先获取锁。但不同的是,使用Condition前,需要先通过锁去创建Condition

ReentrantLock中提供的Condition为例,首先是创建Condition对象:

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

然后是获取锁并调用await方法:

new Thread(() -> {
  lock.lock();
  try {
    condition.await();
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }
  lock.unlock();
}

最后,通过调用singalAll唤醒全部阻塞中的线程:

new Thread(() -> {
  lock.lock();
  condition.signalAll();
  lock.unlock();
}

ConditionObject的源码分析

作为接口Condition非常惨,因为在Java中只有AQS中的内部类ConditionObject实现了Condition接口:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  
  public class ConditionObject implements Condition, java.io.Serializable {
    private transient Node firstWaiter;
    
    private transient Node lastWaiter;
  }
  
  static final class Node {
    // 省略
  }
}

ConditionObject只有两个Node类型的字段,分别是链式结构中的头尾节点,ConditionObject就是通过它们实现的等待队列。那么ConditionObject的等待队列起到了怎样的作用呢?是类似于AQS中的排队机制吗?带着这两个问题,我们正是开始源码的分析。

await方法的实现

Condition接口中定义了4个线程等待的方法:

  • void await() throws InterruptedException
  • void awaitUninterruptibly();
  • long awaitNanos(long nanosTimeout) throws InterruptedException;
  • boolean await(long time, TimeUnit unit) throws InterruptedException;
  • boolean awaitUntil(Date deadline) throws InterruptedException;

方法虽然很多,但它们之间的差异较小,只体现在时间的处理上,我们看其中最常用的方法:

public final void await() throws InterruptedException {
  // 线程中断,抛出异常
  if (Thread.interrupted()) {
    throw new InterruptedException();
  }
  // 注释1:加入到Condition的等待队列中
  Node node = addConditionWaiter();
  // 注释2:释放持有锁(调用AQS的release)
  int savedState = fullyRelease(node);
  int interruptMode = 0;
  // 注释3:判断是否在AQS的等待队列中
  while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
    // 中断时退出方法
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
      break;
    }
  }
  
  // 加入到AQS的等待队列中,调用AQS的acquireQueued方法
  if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
    interruptMode = REINTERRUPT;
  }
  
  // 断开与Condition队列的联系
  if (node.nextWaiter != null) {
    unlinkCancelledWaiters();
  }
  
  if (interruptMode != 0) {
   reportInterruptAfterWait(interruptMode);
  }
}

注释1的部分,调用addConditionWaiter方法添加到Condition队列中:

private Node addConditionWaiter() {
  // 判断当前线程是否为持有锁的线程
  if (!isHeldExclusively()) {
    throw new IllegalMonitorStateException();
  }
  
  // 获取Condition队列的尾节点
  Node t = lastWaiter;
  // 断开不再位于Condition队列的节点
  if (t != null && t.waitStatus != Node.CONDITION) {
    unlinkCancelledWaiters();
    t = lastWaiter;
  }
  
  // 创建Node.CONDITION模式的Node节点
  Node node = new Node(Node.CONDITION);
  if (t == null) {
    // 队列为空的场景,将node设置为头节点
    firstWaiter = node;
  } else {
    // 队列不为空的场景,将node添加到尾节点的后继节点上
    t.nextWaiter = node;
  }
  // 更新尾节点
  lastWaiter = node;
  return node;
}

可以看到,Condition的队列是一个朴实无华的双向链表,每次调用addConditionWaiter方法,都会加入到Condition队列的尾部。

注释2的部分,释放线程持有的锁,同时移出AQS的队列,内部调用了AQS的release方法:

=final int fullyRelease(Node node) {
  try {
    int savedState = getState();
    if (release(savedState)) {
      return savedState;
    }
    throw new IllegalMonitorStateException();
  } catch (Throwable t) {
    node.waitStatus = Node.CANCELLED;
    throw t;
  }
}

因为已经分析过AQS的release方法和ReentrantLock实现的tryRelease方法,这里我们就不过多赘述了。

注释3的部分,isOnSyncQueue判断当前线程是否在AQS的等待队列中,我们来看此时存在的情况:

  • 如果isOnSyncQueue返回false,即线程不在AQS的队列中,进入自旋,调用LockSupport#park暂停线程;
  • 如果isOnSyncQueue返回true,即线程在AQS的队列中,不进入自旋,执行后续逻辑。

结合注释1和注释2的部分,Condition#await的实现原理了就很清晰了:

  • Condition与AQS分别维护了一个等待队列,而且是互斥的,即同一个节点只会出现在一个队列中
  • 当调用Condition#await时,将线程添加到Condition的队列中(注释1),同时从AQS队列中移出(注释2);
  • 接着判断线程位于的队列:
    • 位于Condition队列中,该线程需要被暂停,调用LockSupport#park
    • 位于AQS队列中,该线程正在等待获取锁。

基于以上的结论,我们已经能够猜到唤醒方法Condition#signalAll的原理了:

  • 将线程从Condition队列中移出,并添加到AQS的队列中;
  • 调用LockSupport.unpark唤醒线程。

至于这个猜想是否正确,我们接着来看唤醒方法的实现。

Tips:如果忘记了AQS中相关方法是如何实现的,可以回顾下《AQS的今生,构建出JUC的基础》。

signal和signalAll方法的实现

来看signalsignalAll的源码:

// 唤醒一个处于等待中的线程
public final void signal() {
  if (!isHeldExclusively()) {
    throw new IllegalMonitorStateException();
  }
  // 获取Condition队列中的第一个节点
  Node first = firstWaiter;
  if (first != null) {
    // 唤醒第一个节点
    doSignal(first);
  }
}

// 唤醒全部处于等待中的线程
public final void signalAll() {
    if (!isHeldExclusively()){
      throw new IllegalMonitorStateException();
    }
        
    Node first = firstWaiter;
    if (first != null) {
      // 唤醒所有节点
      doSignalAll(first);
    }  
}

两个方法唯一的差别在于头节点不为空的场景下,是调用doSignal唤醒一个线程还是调用doSignalAll唤醒所有线程:

private void doSignal(Node first) {
  do {
    // 更新头节点
    if ( (firstWaiter = first.nextWaiter) == null) {
      // 无后继节点的场景
      lastWaiter = null;
    }
    // 断开节点的连接
    first.nextWaiter = null;
    // 唤醒头节点
  } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

private void doSignalAll(Node first) {
  // 将Condition的队列置为空
  lastWaiter = firstWaiter = null;
  do {
    // 断开链接
    Node next = first.nextWaiter;
    first.nextWaiter = null;
    // 唤醒当前头节点
    transferForSignal(first);
    // 更新头节点
    first = next;
  } while (first != null);
}

可以看到,无论是doSignal还是doSignalAll都只是将节点移出Condition队列,而真正起到唤醒作用的是transferForSignal方法,从方法名可以看到该方法是通过“转移”进行唤醒的,我们来看源码:

final boolean transferForSignal(Node node) {
  // 通过CAS替换node的状态
  // 如果替换失败,说明node不处于Node.CONDITION状态,不需要唤醒
  if (!node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
    return false;
  }
  // 将节点添加到AQS的队列的队尾
  // 并返回老队尾节点,即node的前驱节点
  Node p = enq(node);
  int ws = p.waitStatus;
  // 对前驱节点状态的判断
  if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL)) {
    LockSupport.unpark(node.thread);
  }
  return true;
}

transferForSignal方法中,调用enq方法将node重新添加到AQS的队列中,并返回node的前驱节点,随后对前驱节点的状态进行判断:

  • \(ws > 0\)时,前驱节点处于Node.CANCELLED状态,前驱节点退出锁的争抢,node可以直接被唤醒;
  • \(ws \leq 0\)时,通过CAS修改前驱节点的状态为Node.SIGNAL,设置失败时,直接唤醒node

《AQS的今生,构建出JUC的基础》中介绍了waitStatus的5种状态,其中Node.SIGNAL状态表示需要唤醒后继节点。另外,在分析shouldParkAfterFailedAcquire方法的源码时,我们知道在进入AQS的等待队列时,需要将前驱节点的状态更新为Node.SIGNAL

最后来看enq的实现:

private Node enq(Node node) {
  for (;;) {
    // 获取尾节点
    Node oldTail = tail;
    if (oldTail != null) {
      // 更新当前节点的前驱节点
      node.setPrevRelaxed(oldTail);
      // 更新尾节点
      if (compareAndSetTail(oldTail, node)) {
        oldTail.next = node;
        // 返回当前节点的前驱节点(即老尾节点)
        return oldTail;
      }
    } else {
      initializeSyncQueue();
    }
  }
}

enq的实现就非常简单了,通过CAS更新AQS的队列尾节点,相当于添加到AQS的队列中,并返回尾节点的前驱节点。好了,唤醒方法的源码到这里就结束了,是不是和我们当初的猜想一模一样呢?

图解ConditionObject原理

功能上,Condition实现了AQS版Object#waitObject#notify,用法上也与之相似,需要先获取锁,即需要在lockunlock之间调用。原理上,简单来说就是线程在AQS的队列和Condition的队列之间的转移

线程t持有锁

假设有线程t已经获取了ReentrantLock,线程t1,t2和t3正在AQS的队列中等待,我们可以得到这样的结构:
17.AQS中的Condition是什么?

线程t执行Condition#await

如果线程t中调用了Condition#await方法,线程t进入Condition的等待队列中,线程t1获取ReentrantLock,并从AQS的队列中移出,结构如下:
17.AQS中的Condition是什么?

线程t1执行Condition#await

如果线程t1中也执行了Condition#await方法,同样线程t1进入Condition队列中,线程t2获取到ReentrantLock,结构如下:
17.AQS中的Condition是什么?

线程t2执行Condition#signal

如果线程t2执行了Condition#signal,唤醒Condition队列中的第一个线程,此时结构如下:
17.AQS中的Condition是什么?

通过上面的流程,我们就可以得到线程是如何在Condition队列与AQS队列中转移的:
17.AQS中的Condition是什么?

结语

关于Condition的内容到这里就结束了,无论是理解,使用还是剖析原理,Condition的难度并不高,只不过大家可能平时用得比较少,因此多少有些陌生。

最后,截止到文章发布,我应该是把开头两道题目的题解写完了吧~~


好了,今天就到这里了,Bye~~文章来源地址https://www.toymoban.com/news/detail-446204.html

到了这里,关于17.AQS中的Condition是什么?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 为什么flink那么受欢迎?

           我们知道,Storm已经不流行了,目前几乎没有公司用。        对于大数据开发,主流的就是Hadoop Spark和Flink,一般学习顺序也都是Hadoop——spark——Flink。        现在也有很多人说Spark已经不行了,更倾向于学习和使用Flink。那是因为一些大厂例如阿里主要是使用F

    2024年01月23日
    浏览(33)
  • 《Windows不欢迎你,你爱用什么系统就用什么去吧》

    Windows不欢迎你 你爱用什么系统就用什么去吧 当你开机时,你就能看到这样一行字,点确定之后啥事没有,可以继续登录 那这是这是怎么实现的呢? 注:此实验在虚拟机下测试过,win10和win11都可以用 这就要说到windows很强大的一个管理程序—— 注册表 注册表是专门用来存放

    2024年02月04日
    浏览(22)
  • Web3 是什么?为何应该关注?

    当我开始我的职业生涯时,“Web2.0”还是一个热门的新事物。 当我开始我的职业生涯时,正值互联网快速发展的时期,人们谈论的是“Web2.0”,这一概念引发了许多关于用户参与、社交媒体和在线合作的讨论。然而,随着时间的推移,又有了一个新的概念进入人们的视野:

    2024年02月12日
    浏览(28)
  • 学Java线程,你不知道什么是AQS?一文带你了解Java多线程同步的灵魂

    关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。 我们继续总结学习 Java基础知识 ,温故知新。 CLH(Craig, Landin, and Hagersten locks)是一种自旋锁,能确保无饥饿性,提

    2024年02月16日
    浏览(33)
  • 学Java线程,你不知道什么是AQS?一文带你进入Java多线程同步的灵魂-AbstractQueuedSynchronizer

    关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。 我们继续总结学习 Java基础知识 ,温故知新。 CLH(Craig, Landin, and Hagersten locks)是一种自旋锁,能确保无饥饿性,提

    2024年02月16日
    浏览(25)
  • Java 21中的两个值得关注的Bug修复

    在Java 21中,除了推出很多新特性之外,一些Bug修复,也需要注意一下。因为这些改变可能在升级的时候,造成影响。 比如:对于 Double.String(1e23) : 在Java 19后,输出内容为: 1.0E23 在Java 18中,输出内容为: 9.999999999999999E22 欢迎关注,持续更新的Java新特性专栏 IdentityHashMap 是

    2024年02月05日
    浏览(35)
  • 7年时间,从功能测试到测试开发月薪30K,有志者事竟成

    突破自己的技术瓶颈并不是一蹴而就,还是需要看清楚一些东西,这里也有一些经验和见解跟大家分享一下。同样是职场人士,我也有我的经历和故事。在工作期间 ,我有过2年加薪5次的小小“战绩”(同期进入公司的员工,加薪不超过2次的员工大有人在),7年的时间顺利

    2024年02月07日
    浏览(45)
  • 嵌入式软件开发工程师应该关注芯片数据手册中的哪些信息

    1. 芯片的架构和处理器类型:了解芯片的架构和处理器类型可以帮助开发人员选择合适的开发工具和编程语言。 2. 芯片的时钟频率和电源要求:了解芯片的时钟频率和电源要求可以帮助开发人员设计合适的电路和电源系统。 3. 芯片的存储器类型和容量:了解芯片的存储器类

    2024年02月08日
    浏览(39)
  • A Beginner‘s Guide to Apache Kafka: 什么是Kafka、它为什么如此受欢迎、它在哪些场景下可以应用、以及一些基本概念和术语

    作者:禅与计算机程序设计艺术 Apache Kafka(以下简称Kafka)是一个开源分布式流处理平台,它被设计用来实时传输大量的数据,从而能够实时的对数据进行处理并提取价值。本文通过梳理,引导读者了解什么是Kafka、它为什么如此受欢迎、它在哪些场景下可以应用、以

    2024年02月09日
    浏览(51)
  • 【Redis从头学-7】Redis中的Set数据类型实战场景之用户画像去重、共同关注、专属粉丝

    🧑‍💻作者名称:DaenCode 🎤作者简介:啥技术都喜欢捣鼓捣鼓,喜欢分享技术、经验、生活。 😎人生感悟:尝尽人生百味,方知世间冷暖。 📖所属专栏:Redis从头学 之前的篇章对Redis的String、List、Hash数据类型已经做出了具体分析,并举例说明了其具体的实战场景本文就

    2024年02月12日
    浏览(24)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包