面试官:呦,小伙子来的挺早啊!

Hydra:那是,不能让您等太久了啊(别废话了快开始吧,还赶着去下一场呢)。

面试官:前面两轮表现还不错,那我们今天继续说说队列中的SynchronousQueue吧。

Hydra:好的,SynchronousQueue和之前介绍过的队列相比,稍微有一些特别,必须等到队列中的元素被消费后,才能继续向其中添加新的元素,因此它也被称为无缓冲的等待队列。

我还是先写一个例子吧,创建两个线程,生产者线程putThreadSynchronousQueue中放入元素,消费者线程takeThread从中取走元素:

SynchronousQueue<Integer> queue=new SynchronousQueue<>(true);

Thread putThread=new Thread(()->{
for (int i = 0; i <= 2; i++) {
try {
System.out.println("put thread put:"+i);
queue.put(i);
System.out.println("put thread put:"+i+" awake");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread takeThread=new Thread(()->{
int j=0;
while(j<2){
try {
j=queue.take();
System.out.println("take from putThread:"+j);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}); putThread.start();
Thread.sleep(1000);
takeThread.start();

执行上面的代码,查看结果:

put thread put:0
take from putThread:0
put thread put:0 awake
put thread put:1
take from putThread:1
put thread put:1 awake
put thread put:2
take from putThread:2
put thread put:2 awake

可以看到,生产者线程在执行put方法后就被阻塞,直到消费者线程执行take方法对队列中的元素进行了消费,生产者线程才被唤醒,继续向下执行。简单来说运行流程是这样的:

面试官:就这?应用谁不会啊,不讲讲底层原理就想蒙混过关?

Hydra:别急啊,我们先从它的构造函数说起,根据参数不同,SynchronousQueue分为公平模式和非公平模式,默认情况下为非公平模式

public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

我们先来看看公平模式吧,该模式下底层使用的是TransferQueue队列,内部节点由QNode构成,定义如下:

volatile QNode next;          // next node in queue
volatile Object item; // CAS'ed to or from null
volatile Thread waiter; // to control park/unpark
final boolean isData;
QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}

item用来存储数据,isData用来区分节点是什么类型的线程产生的,true表示是生产者,false表示是消费者,是后面用来进行节点匹配complementary )的关键。在SynchronousQueue中匹配是一个非常重要的概念,例如一个线程先执行put产生了一个节点放入队列,另一个线程再执行take产生了一个节点,这两个不同类型的节点就可以匹配成功。

面试官:可是我看很多资料里说SynchronousQueue是一个不存储元素的阻塞队列,这点你是怎么理解的?

Hydra:通过上面节点中封装的属性,可以看出SynchronousQueue的队列中封装的节点更多针对的不是数据,而是要执行的操作,个人猜测这个说法的出发点就是队列中存储的节点更多偏向于操作这一属性。

面试官:好吧,接着往下说队列的结构吧。

Hydra:TransferQueue中主要定义的属性有下面这些:

transient volatile QNode head;
transient volatile QNode tail;
transient volatile QNode cleanMe;
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}

比较重要的有头节点head、尾节点tail、以及用于标记下一个要删除的节点的cleanMe节点。在构造函数初始化中创建了一个节点,注释中将它称为dummy node,也就是伪造的节点,它的作用类似于AQS中的头节点的作用,实际操作的节点是它的下一个节点。

要说SynchronousQueue,真是一个神奇的队列,不管你调用的是putoffer,还是takepoll,它都一概交给核心的transfer方法去处理,只不过参数不同。今天我们抛弃源码,通过画图对它进行分析,首先看一下方法的定义:

E transfer(E e, boolean timed, long nanos);

面试官:呦呵,就一个方法?我倒要看看它是怎么区分实现的入队和出队操作…

Hydra:在方法的参数中,timednanos用于标识调用transfer的方法是否是能够超时退出的,而e是否为空则可以说明是生产者还是消费者调用的此方法。如果e不为null,是生产者调用,如果enull则是消费者调用。方法的整体逻辑可以分为下面几步:

1、若队列为空,或队列中的尾节点类型和自己的类型相同,那么准备封装一个新的QNode添加到队列中。在添加新节点到队尾的过程中,并没有使用synchronizedReentrantLock,而是通过CAS来保证线程之间的同步。

在添加新的QNode到队尾前,会首先判断之前取到的尾节点是否发生过改变,如果有改变的话那么放弃修改,进行自旋,在下一次循环中再次判断。当检查队尾节点没有发生改变后,构建新的节点QNode,并将它添加到队尾。

2、当新节点被添加到队尾后,会调用awaitFulfill方法,会根据传递的参数让线程进行自旋或直接挂起。方法的定义如下,参数中的timedtrue时,表示这是一个有等待超时的方法。

Object awaitFulfill(QNode s, E e, boolean timed, long nanos);

awaitFulfill方法中会进行判断,如果新节点是head节点的下一个节点,考虑到可能很快它就会完成匹配后出队,先不将它挂起,进行一定次数的自旋,超过自旋次数的上限后再进行挂起。如果不是head节点的下一个节点,避免自旋造成的资源浪费,则直接调用parkparkNanos挂起线程。

3、当挂起的线程被中断或到达超时时间,那么需要将节点从队列中进行移除,这时会执行clean()方法。如果要被删除的节点不是链表中的尾节点,那么比较简单,直接使用CAS替换前一个节点的next指针。

如果要删除的节点是链表中的尾节点,就会有点复杂了,因为多线程环境下可能正好有其他线程正在向尾节点后添加新的节点,这时如果直接删除尾节点的话,会造成后面节点的丢失。

这时候就会用到TransferQueue中定义的cleanMe标记节点了,cleanMe的作用就是当要被移除的节点是队尾节点时,用它来标记队尾节点的前驱节点。具体在执行过程中,又会分为两种情况:

  • cleanMe节点为null,说明队列在之前没有标记需要删除的节点。这时会使用cleanMe来标识该节点的前驱节点,标记完成后退出clean方法,当下一次执行clean方法时才会删除cleanMe的下一个节点。

  • cleanMe节点不为null,那么说明之前已经标记过需要删除的节点。这时删除cleanMe的下一个节点,并清除当前cleanMe标记,并再将当前节点未修改前的前驱节点标记为cleanMe。注意,当前要被删除的节点的前驱节点不会发生改变,即使这个前驱节点已经在逻辑上从队列中删除掉了。

执行完成clean方法后,transfer方法会直接返回null,说明入队操作失败。

面试官:讲了这么多,入队的还都是一个类型的节点吧?

Hydra:是的,TransferQueue队列中,只会存在一个类型的节点,如果有另一个类型的节点过来,那么就会执行出队的操作了。

面试官:好吧,那你接着再说说出队方法吧。

Hydra:相对入队来说,出队的逻辑就比较简单了。因为现在使用的是公平模式,所以当队列不为空,且队列的head节点的下一个节点与当前节点匹配成功时,进行出队操作,唤醒head节点的下一个节点,进行数据的传递。

根据队列中节点类型的不同,可以分为两种情况进行分析:

1、如果head节点的下一个节点是put类型,当前新节点是take类型。take线程取出put节点的item的值,并将其item变为null,然后推进头节点,唤醒被挂起的put线程,take线程返回item的值,完成数据的传递过程。

head节点的下一个节点被唤醒后,会推进head节点,虽然前面说过队列的head节点是一个dummy节点,并不存储数据,理论上应该将第二个节点直接移出队列,但是源码中还是将head节点出队,将原来的第二个节点变成了新的head节点。

2、同理,如果head节点的下一个节点是take类型,当前新节点是put类型。put线程会将take节点的item设为自己的数据值,然后推进头节点,并唤醒挂起的take线程,唤醒的take线程最终返回从put线程获得的item的值。

此外,在take线程唤醒后,会将自己QNodeitem指针指向自己,并将waiter中保存的线程置为null,方便之后被gc回收。

面试官:也就是说,在代码中不一定非要生产者先去生产产品,也可以由消费者先到达后进行阻塞等待?

Hydra:是的,两种线程都可以先进入队列。

面试官:好了,公平模式下我是明白了,我去喝口水,给你十分钟时间,回来我们聊聊非公平模式的实现吧。

Hydra:……

面试侃集合 | SynchronousQueue公平模式篇的更多相关文章

  1. 面试侃集合 | SynchronousQueue非公平模式篇

    面试官:好了,你也休息了十分钟了,咱们接着往下聊聊SynchronousQueue的非公平模式吧. Hydra:好的,有了前面公平模式的基础,非公平模式理解起来就非常简单了.公平模式下,Synchro ...

  2. 面试侃集合 | ArrayBlockingQueue篇

    面试官:平常在工作中你都用过什么什么集合? Hydra:用过 ArrayList.HashMap,呃-没有了 面试官:好的,回家等通知吧- 不知道大家在面试中是否也有过这样的经历,工作中仅仅用过的那么 ...

  3. 面试侃集合 | LinkedBlockingQueue篇

    面试官:好了,聊完了ArrayBlockingQueue,我们接着说说LinkedBlockingQueue吧 Hydra:还真是不给人喘口气的机会,LinkedBlockingQueue是一个基于链 ...

  4. 面试侃集合 | DelayQueue篇

    面试官:好久不见啊,上次我们聊完了PriorityBlockingQueue,今天我们再来聊聊和它相关的DelayQueue吧. Hydra:就知道你前面肯定给我挖了坑,DelayQueue也是一个无 ...

  5. 【JAVA秒会技术之秒杀面试官】秒杀Java面试官——集合篇(一)

    [JAVA秒会技术之秒杀面试官]秒杀Java面试官——集合篇(一) [JAVA秒会技术之秒杀面试官]JavaEE常见面试题(三) http://blog.csdn.net/qq296398300/ar ...

  6. 图解SynchronousQueue原理详解-非公平模式

    SynchronousQueue原理详解-非公平模式 开篇 说明:本文分析采用的是jdk1.8 约定:下面内容中Ref-xxx代表的是引用地址,引用对应的节点 前面已经讲解了公平模式的内容,今天来讲解 ...

  7. 图解SynchronousQueue原理-公平模式

    SynchronousQueue原理详解-公平模式 一.介绍 SynchronousQueue是一个双栈双队列算法,无空间的队列或栈,任何一个对SynchronousQueue写需要等到一个对Sync ...

  8. Java 面试知识点解析(四)——版本特性篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

  9. Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) JAVA日志的前世今生 .NET MVC采用SignalR更新在线用户数 C#多线程编程系列(五)- 使用任务并行库 C#多线程编程系列(三)- 线程同步 C#多线程编程系列(二)- 线程基础 C#多线程编程系列(一)- 简介

    Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) 一.前言 由于本篇文章较长,所以下面给出内容目录方便跳转阅读,当然也可以用博客页面最右侧的文章目录导航栏进行跳转查阅. 一.前言 ...

随机推荐

  1. isAssignableFrom与instanceof

    isAssignableFrom()方法与instanceof关键字的区别总结为以下两个点: isAssignableFrom()方法是从类继承的角度去判断,instanceof关键字是从实例继承的角 ...

  2. 手把手教你搭建自己的Angular组件库 - DevUI

    摘要:DevUI 是一款面向企业中后台产品的开源前端解决方案,它倡导沉浸.灵活.至简的设计价值观,提倡设计者为真实的需求服务,为多数人的设计,拒绝哗众取宠.取悦眼球的设计.如果你正在开发 ToB 的工 ...

  3. 【MySQL】若sql语句中order by指定了多个字段,则怎么排序?

    举个例子吧:order by id desc,time desc先是按 id 降序排列 (优先)如果 id 字段 有些是一样的话 再按time 降序排列 (前提是满足id降序排列)   order b ...

  4. 【Jwt】JSON Web Token

    一.什么是JSON Web Token: 首先要明确的是JSON Web Token:是一个开放标准,这个标准定义了一种用于简洁,自包含的用于通信双方之间以JSON对象的形式安全传递信息的方法 而我们 ...

  5. jsp JDBC连接MySQL数据库操作标准流程参考

    1. 此案例以帐号密码后台更新维护为例子,对数据库调取数据更新流程进行演示: 代码示例: <%@page import="java.io.IOException"%> ...

  6. Bugku-文件包含2

    文件包含2 目录 文件包含2 题目描述 解题过程 参考 题目描述 没有描述 解题过程 文件包含题目大多都是php环境的, 所以先试试伪协议 发现php://被ban了 继续尝试,发现file://协议 ...

  7. 音视频开发:为什么推荐使用Jetpack CameraX?

    我们的生活已经越来越离不开相机,从自拍到直播,扫码再到VR等等.相机的优劣自然就成为了厂商竞相追逐的赛场.对于app开发者来说,如何快速驱动相机,提供优秀的拍摄体验,优化相机的使用功耗,是一直以来追求 ...

  8. 手机访问电脑本地localhost网页

    项目需要用手机访问电脑本地网页,从而可以调试项目,对代码的理解的快一点 重点 确保手机和电脑在同一个局域网 可以通过手机开热点电脑连接或者电脑开便携式热点手机连接 确保电脑的防火墙是关闭的 打开apa ...

  9. Educational Codeforces Round 101 (Rated for Div. 2)

    A. Regular Bracket Sequence 题意:题目中给(和)还有?,其中?可以转换成为()中的任何一个,并且所给样例中只出现一次(),问能不能括号匹配 思路:直接看第一个和最后一个能不 ...

  10. 传统 BI 如何转大数据数仓

    前几天建了一个数据仓库方向的小群,收集了大家的一些问题,其中有个问题,一哥很想去谈一谈--现在做传统数仓,如何快速转到大数据数据呢?其实一哥知道的很多同事都是从传统数据仓库转到大数据的,今天就结合身边 ...