条件队列是什么?可能很多人和我一样答不出来,不过今天终于搞清楚了!

什么是条件队列

条件队列:当某个线程调用了wait方法,或者通过Condition对象调用了await相关方法,线程就会进入阻塞状态,并加入到对应条件队列中。

等待唤醒机制相关文章中我们提到了条件队列,即当对象获取到同步锁之后,如果调用了wait方法,当前线程会进入到条件队列中,并释放锁。

  1. synchronized(对象){ // 获取锁失败,线程会加入到同步队列中
  2. while(条件不满足){
  3. 对象.wait();// 调用wait方法当前线程加入到条件队列中
  4. }
  5. }

基于synchcronized的内置条件队列存在一些缺陷。每个内置锁都只能有一个相关联的条件队列,因而存在多个线程可能在同一个条件队列上等待不同的条件谓词,并且在最常见的加锁模式下公开条件队列对象。

Java中的锁的实现可以分为两种,一种是基于synchronized的隐式锁,它是基于JVM层面实现的;而另一种则是基于AQS框架在代码层面实现的锁,如ReentrantLock等,在进行并发控制过程中,很多情况下他们都可以相互替代。

其中同步队列和条件队列是AQS中两个比较核心的概念,它们是代码层面实现锁的关键。关于同步队列的内容,我们已经在图解AQS的设计与实现,手摸手带你实现一把互斥锁!中进行了详细的介绍。

与Object配合synchronized相比,基于AQS的Lock&Condition实现的等待唤醒模式更加灵活,支持多个条件队列,支持等待状态中不响应中断以及超时等待功能; 其次就是基于AQS实现的条件队列是"肉眼可见"的,我们可以通过源代码进行debug,而synchronized则是完全隐式的。

同步队列和条件队列

与条件队列密不可分的类则是ConditionObject, 是AQS中实现了Condition接口的内部类,通常配合基于AQS实现的锁一同使用。当线程获取到锁之后,可以调用await方法进入条件队列并释放锁,或者调用singinal方法唤醒对应条件队列中等待时间最久的线程并加入到等待队列中。

在AQS中,线程会被封装成Node对象加入队列中,而条件队列中则复用了同步队列中的Node对象

Condition相关方法和描述

Condition接口一共定义了以下几个方法:

await(): 当前线程进入等待状态,直到被通知(siginal)或中断【和wait方法语义相同】。

awaitUninterruptibly(): 当前线程进入等待状态,直到被通知,对中断不敏感。

awaitNanos(long timeout): 当前线程进入等待状态直到被通知(siginal),中断或超时。

awaitUnitil(Date deadTime): 当前线程进入等待状态直到被通知(siginal),中断或到达某个时间。

signal(): 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition关联的锁【和notify方法语义相同】

signalAll(): 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition关联的锁【和notifyAll方法语义相同】。

条件队列入队操作

当线程获取到锁之后,Condition对象调用await相关的方法,线程会进入到对应的条件队列中。

  1. /**
  2. * 如果当前线程被终端,抛出 InterruptedException 异常
  3. */
  4. public final void await() throws InterruptedException {
  5. if (Thread.interrupted())
  6. throw new InterruptedException();
  7. // 添加当前线程到【条件队列】
  8. Node node = addConditionWaiter();
  9. // 释放已经获取的锁资源,并返回释放前的同步状态
  10. int savedState = fullyRelease(node);
  11. int interruptMode = 0;
  12. // 如果当前节点不在【同步队列】中, 线程进入阻塞状态,等待被唤醒
  13. while (!isOnSyncQueue(node)) {
  14. LockSupport.park(this);
  15. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
  16. break;
  17. }
  18. if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
  19. interruptMode = REINTERRUPT;
  20. if (node.nextWaiter != null) // clean up if cancelled
  21. unlinkCancelledWaiters();
  22. if (interruptMode != 0)
  23. reportInterruptAfterWait(interruptMode);
  24. }

条件队出队操作

Condition对象调用signal或者signalAll方法时,

  1. /**
  2. * 将【条件队列】中第一个有效的元素移除并且添加到【同步队列】中
  3. * 所谓有效指的是非null,并且状态吗
  4. * @param first 条件队列中第一个非空的元素
  5. */
  6. private void doSignal(Node first) {
  7. do {
  8. if ( (firstWaiter = first.nextWaiter) == null)
  9. lastWaiter = null;
  10. first.nextWaiter = null;
  11. // 将条件队列中等待最久的那个有效元素添加到同步队列中
  12. } while (!transferForSignal(first) &&
  13. (first = firstWaiter) != null);
  14. }
  15. /**
  16. * 将条件队列中的节点转换到同步队列中
  17. */
  18. final boolean transferForSignal(Node node) {
  19. /*
  20. * If cannot change waitStatus, the node has been cancelled.
  21. * 如果节点的等待状态不能被修改,说明当前线程已经被取消等待【多个线程执行siginal时会出现的情况】
  22. */
  23. if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
  24. return false;
  25. /*
  26. * 加入到【同步队列】中,并且尝试将前驱节点设置为可唤醒状态
  27. */
  28. Node p = enq(node); // 将node添加到同步队列中,并返回它的前驱节点
  29. int ws = p.waitStatus;
  30. // 如果前驱节点不需要唤醒,或者设置状态为‘唤醒’失败,则唤醒线程时期重新争夺同步状态
  31. if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
  32. LockSupport.unpark(node.thread);
  33. return true;
  34. }

实现阻塞队列

  1. 自定义互斥锁升级,增加获取Condition对象接口。
  1. /**
  2. * 自定义互斥锁
  3. *
  4. * @author cruder
  5. * @time 2019/11/29 9:43
  6. */
  7. public class MutexLock {
  8. private static final Sync STATE_HOLDER = new Sync();
  9. /**
  10. * 通过Sync内部类来持有同步状态, 当状态为1表示锁被持有,0表示锁处于空闲状态
  11. */
  12. private static class Sync extends AbstractQueuedSynchronizer {
  13. /**
  14. * 是否被独占, 有两种表示方式
  15. * 1. 可以根据状态,state=1表示锁被占用,0表示空闲
  16. * 2. 可以根据当前独占锁的线程来判断,即getExclusiveOwnerThread()!=null 表示被独占
  17. */
  18. @Override
  19. protected boolean isHeldExclusively() {
  20. return getExclusiveOwnerThread() != null;
  21. }
  22. /**
  23. * 尝试获取锁,将状态从0修改为1,操作成功则将当前线程设置为当前独占锁的线程
  24. */
  25. @Override
  26. protected boolean tryAcquire(int arg) {
  27. if (compareAndSetState(0, 1)) {
  28. setExclusiveOwnerThread(Thread.currentThread());
  29. return true;
  30. }
  31. return false;
  32. }
  33. /**
  34. * 释放锁,将状态修改为0
  35. */
  36. @Override
  37. protected boolean tryRelease(int arg) {
  38. if (getState() == 0) {
  39. throw new UnsupportedOperationException();
  40. }
  41. setExclusiveOwnerThread(null);
  42. setState(0);
  43. return true;
  44. }
  45. //【新增代码】
  46. final ConditionObject newCondition() {
  47. return new ConditionObject();
  48. }
  49. }
  50. /**
  51. * 下面的实现Lock接口需要重写的方法,基本是就是调用内部内Sync的方法
  52. */
  53. public void lock() {
  54. STATE_HOLDER.acquire(1);
  55. }
  56. public void unlock() {
  57. STATE_HOLDER.release(1);
  58. }
  59. // 【新增代码】 获取条件队列
  60. public Condition newCondition(){
  61. return STATE_HOLDER.newCondition();
  62. }
  63. }
  1. 基于自定义互斥锁,实现阻塞队列。阻塞队列具有两个特点:
  • 添加元素到队列中, 如果队列已满会使得当前线程阻塞【加入到条件队列-队列不满】,直到队列不满为止
  • 移除队列中的元素,当队列为空时会使当前线程阻塞【加入到条件队列-队列不空】,直到队列不为空为止
  1. /**
  2. * 有界阻塞阻塞队列
  3. *
  4. * @author Jann Lee
  5. * @date 2019-12-11 22:20
  6. **/
  7. public class BoundedBlockingQueue<T> {
  8. /**
  9. * list作为底层存储结构
  10. */
  11. private List<T> dataList;
  12. /**
  13. * 队列的大小
  14. */
  15. private int size;
  16. /**
  17. * 锁,和条件变量
  18. */
  19. private MutexLock lock;
  20. /**
  21. * 队列非空 条件变量
  22. */
  23. private Condition notEmpty;
  24. /**
  25. * 队列未满 条件变量
  26. */
  27. private Condition notFull;
  28. public BoundedBlockingQueue(int size) {
  29. dataList = new ArrayList<>();
  30. lock = new MutexLock();
  31. notEmpty = lock.newCondition();
  32. notFull = lock.newCondition();
  33. this.size = size;
  34. }
  35. /**
  36. * 队列中添加元素 [只有队列未满时才可以添加,否则需要等待队列变成未满状态]
  37. */
  38. public void add(T data) throws InterruptedException {
  39. lock.lock();
  40. try {
  41. // 如果队列已经满了, 需要在等待“队列未满”条件满足
  42. while (dataList.size() == size) {
  43. notFull.await();
  44. }
  45. dataList.add(data);
  46. Thread.sleep(2000);
  47. notEmpty.signal();
  48. } finally {
  49. lock.unlock();
  50. }
  51. }
  52. /**
  53. * 移除队列的第一个元素[只有队列非空才可以移除,否则需要等待变成队列非空状态]
  54. */
  55. public T remove() throws InterruptedException {
  56. lock.lock();
  57. try {
  58. // 如果为空, 需要在等待“队列非空”条件满足
  59. while (dataList.isEmpty()) {
  60. notEmpty.await();
  61. }
  62. T result = dataList.remove(0);
  63. notFull.signal();
  64. return result;
  65. } finally {
  66. lock.unlock();
  67. }
  68. }
  69. }

总结

  1. 条件队列和同步队列在Java中有两种实现,synchronized关键字以及基于AQS
  2. 每个(基于synchronized的)内置锁都只能有一个相关联的条件队列,会存在多个线程可能在同一个条件队列上等待不同的条件谓词;而(基于AQS实现的)显式锁支持多个条件队列
  3. 与wait,notify,notifyAll 对应的方法时Conditoin接口中的await,signal,signalAll,他们具有相同的语义

最后敬上一个关注了也没有错的公众号~

浅谈Java中的Condition条件队列,手摸手带你实现一个阻塞队列!的更多相关文章

  1. 浅谈Java中set.map.List的区别

    就学习经验,浅谈Java中的Set,List,Map的区别,对JAVA的集合的理解是想对于数组: 数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),JAVA集合可以存储和操 ...

  2. Java基础学习总结(29)——浅谈Java中的Set、List、Map的区别

    就学习经验,浅谈Java中的Set,List,Map的区别,对JAVA的集合的理解是想对于数组: 数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),JAVA集合可以存储和操 ...

  3. 浅谈Java中的equals和==(转)

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str ...

  4. 浅谈Java中的对象和引用

    浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...

  5. 浅谈Java中的equals和==

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: String str1 = new String("hello"); String str2 = ...

  6. 浅谈Java中的深拷贝和浅拷贝(转载)

    浅谈Java中的深拷贝和浅拷贝(转载) 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: ...

  7. 浅谈Java中的深拷贝和浅拷贝

    转载: 浅谈Java中的深拷贝和浅拷贝 假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(bool ...

  8. 【转】浅谈Java中的hashcode方法(这个demo可以多看看)

    浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native i ...

  9. 浅谈Java中的final关键字

    浅谈Java中的final关键字 谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来 ...

随机推荐

  1. LGOJP1850 换教室

    题目地址 https://www.luogu.org/problem/P1850 题解 这题的转移其实挺好想的但是方程特别长...真的特别长... 首先设\(f[i,j,0/1]\)表示当前在第\(i ...

  2. 注解@Transient

     @Transient表示该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性. 如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient,否则,ORM框架默认其注解为@Bas ...

  3. treegrid 折叠全部节点

    $(".easyui-treegrid").treegrid({ url: '@Url.Action("GetDataDictionaryList", &quo ...

  4. 数据库jdbc链接:mysql, oracle, postgresql

    #db mysql#jdbc.driver=com.mysql.jdbc.Driver#jdbc.url=jdbc:mysql://localhost:3306/mysql?&useUnico ...

  5. MySQL——时间戳和时间的转化

    前言 Mysql中时间戳和时间的转化 时间转时间戳 select unix_timestamp('2019-7-29 14:23:25'); 时间戳转时间 select from_unixtime(1 ...

  6. CORS 跨域 node |XMLHttpRequest 跨域提交数据 node

    node服务端 app.post('/getdata',function(req,res,next){ req.setEncoding('utf8'); res.setHeader('Access-C ...

  7. 101 More Security Best Practices for Kubernetes

    https://rancher.com/blog/2019/2019-01-17-101-more-kubernetes-security-best-practices/ The CNCF recen ...

  8. MySql添加字段命令

    使用ALTER TABLE命令来向一个表添加字段,示例如下: -- 向t_user表添加user_age字段 ) DEFAULT NULL COMMENT '年龄' AFTER user_email; ...

  9. 洛谷 P4281 [AHOI2008] 紧急集合 题解

    挺好的一道题,本身不难,就把求两个点的LCA变为求三个点两两求LCA,不重合的点才是最优解.值得一提的是,最后对答案的处理运用差分的思想:假设两点 一点深度为d1,另一点 深度为d2,它们LCA深度为 ...

  10. gerrit配置跳过审核直接push到gitlab

    项目中有存放项目相关的文档,这些项目需要配置跳过审核再提交的操作.现在需要给某些组配置不审核直接提交的权限 方法: 使用管理员账号,到 projects -> access 页面下配置 refe ...