面试官:嗯,你也休息了十分钟,下面说说SynchronousQueue的不公平模式。Hydra:嗯,有了前面公平模型的基础,不公平模型就很容易理解了。在公平模式下,SynchronousQueue底层使用的是TransferQueue,它是一个先进先出的队列。与非公平模式不同的是,底层采用后进先出的TransferStack栈来实现。接下来我们写个例子看看效果。首先创建3个线程并使用put方法向SynchronousQueue中插入数据,然后使用3个线程调用take方法:SynchronousQueuequeue=newSynchronousQueue<>(false);@AllArgsConstructorclassPutThreadimplementsRunnable{inti;@SneakyThrows@Overridepublicvoidrun(){queue.put(i);System.out.println("putThread"+i+"end");}}classTakeThreadimplementsRunnable{@SneakyThrows@Overridepublicvoidrun(){System.out.println("takeThreadtake:"+queue.take());}}for(inti=1;i<=3;i++){newThread(newPutThread(i)).start();Thread.sleep(1000);}for(inti=1;i<=3;i++){newThread(newTakeThread()).start();Thread.sleep(1000);}运行上面的代码并查看结果:takeThreadtake:3putThread3endtakeThreadtake:2putThread2endtakeThreadtake:1putThread1end可以看出,生产者线程在执行完put之后会阻塞,直到有消费者线程调用take方法取数据,被阻塞的线程才会被唤醒。而且数据出队和入队的顺序是相反的,即非公平模式采用后进先出的顺序。原图采访者:就是把结构从队列改成了栈。真的那么简单吗?Hydra:没有,底层节点和入栈入栈的逻辑都做了相应的改动。让我们先看看节点。前面的公平模式下,队列中的节点是QNode,非公平模式下,栈中的节点是SNode,定义如下:volatileSNodeext;//指向下一个节点的指针volatileSNodematch;//存储和匹配itNodevolatileThreadwaiter;//保存阻塞的线程Objectitem;intmode;SNode(Objectitem){this.item=item;}类似于QNode,如果是生产者建的节点,则item不为空,如果是a由消费者生成的节点,则项目为空。另外还有一个mode属性用来表示节点的状态,它使用TransferStack中定义的三个常量来表示不同的状态:staticfinalintREQUEST=0;//ConsumerstaticfinalintDATA=1;//ProducerstaticfinalintFULFILLING=2;//Match中间态TransferStack没有带参数的构造函数,用一个head节点来标记栈顶节点:volatileSNodehead;面试官:这里说一下基本结构,还是老规矩,我们从enqueue操作开始分析。Hydra:当栈为空,或者栈顶元素的类型与自身相同时,会先创建一个SNode节点,其下一个节点指向当前栈顶的头部,然后是头部指针将指向自身。在这个过程中,使用了CAS来保证线程安全。如果失败则退出,并以循环自旋的形式继续尝试,直到节点成功入栈。用一张图来表示两个线程同时入栈的场景:原图中,节点入栈时,会调用awaitFulfill方法等待匹配操作的到来。在此过程中,该节点对应的线程会自旋或挂起,直到匹配操作的节点自行唤醒,或被其他线程中断,或等待超时。当入栈的节点是栈顶节点,或者节点的类型是FULFILLING匹配状态,那么可能马上就完成了匹配,所以先自旋,当数量达到上限时再挂起超过自旋。但是如果在节点自旋过程中有新的节点被压入栈顶,则非顶层节点的剩余自旋次数会直接清零,线程会被挂起,避免资源浪费。原图采访者:你上面说了,挂起的线程可能会超时或者被中断。这个时候怎么办?Hydra:当这两种情况出现时,SNode都会将match属性设置为自己并退出awaitFulfill方法,然后调用clean方法从栈中清理对应的节点。具体情况可以分为两种情况。先说简单的情况吧。如果要清理栈顶节点,则直接将头节点指向它的下一个节点,即弹出当前栈顶节点。面试官:那要删除的节点不是栈顶节点怎么办?Hydra:如果要删除的节点不是栈顶节点,会有点麻烦。由于栈底层是单向链表结构,需要从栈顶的头节点遍历到被删除节点的后继节点。因此,在清理工作开始之前,用一个过去的节点来标记下一个需要删除的节点,作为结束遍历的标记。然后创建一个标记节点p,最初指向头节点,并开始循环。如果p的下一个节点不是需要删除的节点,则将p后退一个位置,直到找到需要删除的中断或超时节点,然后将p的next指向被删除的下一个节点节点,逻辑上完成链表中节点的删除。原图采访者:单类节点的堆叠我应该说完了。接下来说一下如何实现不同类型节点之间的匹配操作?九头蛇:好的,我们先回顾一下上面的知识。一个节点有一个mode属性来表示它的模式,REQUEST表示它是一个消费者,DATA表示它是一个生产者,FULFILLING表示它处于匹配状态。当一个新的线程调用一个方法时,首先要判断它的类型模式是什么。如果与栈顶头节点的类型不同,且头节点的状态不匹配,则将其状态设置为FULFILLING|mode,压入栈中。然后它会尝试匹配新的头节点和它的下一个节点。如果匹配成功,下一个节点的match属性会被设置为头节点,挂起的下一个节点中的线程会被唤醒。匹配完成后,当前头节点对应的线程会协助头节点前进,将头指向下一个节点的下一个节点,即完成栈顶两个节点的出栈。最后,消费者线程会返回匹配的生产者节点中的item数据值,生产者线程也运行完毕退出。我们画一张图,栈中当前节点为DATA类型,新节点为REQUEST类型,直观感受一下上面的过程:原图采访者:终于讲完了,能不能对SynchronousQueue做一个简单的总结?Hydra:SynchronousQueue基于底层结构实现了线程配对通信的机制。它在其公平模式下使用先进先出(FIFO)队列,在其非公平模式下使用后进先出(LIFO)堆栈,并且SynchronousQueue不使用synchronized或ReentrantLock,而是使用一个大保证并发操作的CAS操作数。可能在平时的工作中我们使用的场景不多,但是在线程池的设计中还是有非常重要的应用场景需要使用SynchronousQueue。面试官:还不错,但是和刚才的公平模式听起来差别不大,也没什么技术含量。这样吧,你明天过来,我们加考,我再给你打分。九头蛇:(我们溜了,找个靠谱的公司吧……)