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

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. 在 Y 分钟内学会 Python

    在 Y 分钟内学会 Python 这是翻译, 原文地址: Learn Python in Y Minutes 在 90 年代初, Python 由 Guido van Rossum 创造, 现在, 它 ...

  2. hdu4909 状态压缩(偶数字符子串)

    题意:       给你一个字符串,里面最多有一个'?','?'可以表示'a' - 'z',也可以什么都不表 示,这里要明确,什么都不表示不是不存在的意思,当aa什么都不表示的时候aa 也不等于aa? ...

  3. Xposed学习一:初探

    学习Xposed框架,在github:https://github.com/rovo89 下载XposedInstaller安装到手机上来管理Xposed的模块. 本文记录根据官方文档(资料1)在an ...

  4. office 2007

    Microsoft office2007免费版几乎包括了Word2007.Excel2007.PowerPoint.Outlook.Publisher.OneNote.Groove.Access.In ...

  5. Ravindrababu Ravula老师的数据结构与算法

    最关键的问题是,作为印度裔,他的英语口音真的真的很好懂!!!而且语速很慢,适合大家学习. 作为一哥热衷于搬砖的小伙,我将他的视频搬运到了B站,大家可以前往我的B站观看,搜索"爱码士Noe&q ...

  6. 【opencv】Java+eclipse+opencv 环境搭建 helloword入门demo

    文章为博主原创,纯属个人理解,如有错误欢迎指出. 如需转载,请注明出处. 引入jar包 引入配置文件 到此环境配置完成!!! 可能会出现的问题: 1. jdk版本不一致导致发生异常.如图 build ...

  7. mybatis增删改返回的int是-2147482646,并不是想要返回结果

    MyBatis发现更新和插入返回值一直为"-2147482646"的错误是由defaultExecutorType设置引起的,如果设置为batch,更新返回值就会丢失,返回结果就只 ...

  8. Mybatis学习之自定义持久层框架(三) 自定义持久层框架:读取并解析配置文件

    前言 前两篇文章分别讲解了JDBC和Mybatis的基本知识,以及自定义持久层框架的设计思路,从这篇文章开始,我们正式来实现一个持久层框架. 新建一个项目 首先我们新建一个maven项目,将其命名为I ...

  9. C++ primer plus读书笔记——第16章 string类和标准模板库

    第16章 string类和标准模板库 1. string容易被忽略的构造函数: string(size_type n, char c)长度为n,每个字母都为c string(const string ...

  10. 笔记·RCNN系相关

    这篇博客总述了从RCNN到Mask RCNN的发展过程 https://blog.csdn.net/heavenpeien/article/details/80534963 简单的说,Fast RCN ...