一、SynchronousQueue简介

  Java 6的并发编程包中的SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。

不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。

特点: 
1、不能在同步队列上进行 peek,因为仅在试图要取得元素时,该元素才存在; 
2、除非另一个线程试图移除某个元素,否则也不能(使用任何方法)添加元素;也不能迭代队列,因为其中没有元素可用于迭代。队列的头是尝试添加到队列中的首个已排队线程元素; 如果没有已排队线程,则不添加元素并且头为 null。 
3、对于其他 Collection 方法(例如 contains),SynchronousQueue 作为一个空集合。此队列不允许 null 元素。
4、它非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。 
5、对于正在等待的生产者和使用者线程而言,此类支持可选的公平排序策略。默认情况下不保证这种排序。 但是,使用公平设置为 true 所构造的队列可保证线程以 FIFO 的顺序进行访问。 公平通常会降低吞吐量,但是可以减小可变性并避免得不到服务。 
6、SynchronousQueue的以下方法: 
    * iterator() 永远返回空,因为里面没东西。 
    * peek() 永远返回null。 
    * put() 往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。 
    * offer() 往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。 
    * offer(2000, TimeUnit.SECONDS) 往queue里放一个element但是等待指定的时间后才返回,返回的逻辑和offer()方法一样。 
    * take() 取出并且remove掉queue里的element(认为是在queue里的。。。),取不到东西他会一直等。 
    * poll() 取出并且remove掉queue里的element(认为是在queue里的。。。),只有到碰巧另外一个线程正在往queue里offer数据或者put数据的时候,该方法才会取到东西。否则立即返回null。 
    * poll(2000, TimeUnit.SECONDS) 等待指定的时间然后取出并且remove掉queue里的element,其实就是再等其他的thread来往里塞。 
    * isEmpty()永远是true。 
    * remainingCapacity() 永远是0。 
    * remove()和removeAll() 永远是false。

SynchronousQueue 内部没有容量,但是由于一个插入操作总是对应一个移除操作,反过来同样需要满足。那么一个元素就不会再SynchronousQueue 里面长时间停留,一旦有了插入线程和移除线程,元素很快就从插入线程移交给移除线程。也就是说这更像是一种信道(管道),资源从一个方向快速传递到另一方 向。显然这是一种快速传递元素的方式,也就是说在这种情况下元素总是以最快的方式从插入着(生产者)传递给移除着(消费者),这在多任务队列中是最快处理任务的方式。在线程池里的一个典型应用是Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

二、 使用示例

  1. package com.dxz.queue.block;
  2.  
  3. import java.util.concurrent.SynchronousQueue;
  4.  
  5. public class SynchronousQueueDemo {
  6. public static void main(String[] args) throws InterruptedException {
  7. final SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>();
  8.  
  9. Thread putThread = new Thread(new Runnable() {
  10. @Override
  11. public void run() {
  12. System.out.println("put thread start");
  13. try {
  14. queue.put(1);
  15. } catch (InterruptedException e) {
  16. }
  17. System.out.println("put thread end");
  18. }
  19. });
  20.  
  21. Thread takeThread = new Thread(new Runnable() {
  22. @Override
  23. public void run() {
  24. System.out.println("take thread start");
  25. try {
  26. System.out.println("take from putThread: " + queue.take());
  27. } catch (InterruptedException e) {
  28. }
  29. System.out.println("take thread end");
  30. }
  31. });
  32.  
  33. putThread.start();
  34. Thread.sleep(1000);
  35. takeThread.start();
  36. }
  37. }

结果:

  1. put thread start
  2. take thread start
  3. take from putThread: 1
  4. take thread end
  5. put thread end

三、实现原理

3.1、阻塞算法实现

3.1.1、使用wait和notify实现

  阻塞算法实现通常在内部采用一个锁来保证多个线程中的put()和take()方法是串行执行的。采用锁的开销是比较大的,还会存在一种情况是线程A持有线程B需要的锁,B必须一直等待A释放锁,即使A可能一段时间内因为B的优先级比较高而得不到时间片运行。所以在高性能的应用中我们常常希望规避锁的使用。

 
  1. package com.dxz.queue.block;
  2.  
  3. public class NativeSynchronousQueue<E> {
  4. boolean putting = false;
  5. E item = null;
  6.  
  7. public synchronized E take() throws InterruptedException {
  8. while (item == null)
  9. wait();
  10. E e = item;
  11. item = null;
  12. notifyAll();
  13. return e;
  14. }
  15.  
  16. public synchronized void put(E e) throws InterruptedException {
  17. if (e == null)
  18. return;
  19. while (putting)
  20. wait();
  21. putting = true;
  22. item = e;
  23. notifyAll();
  24. while (item != null)
  25. wait();
  26. putting = false;
  27. notifyAll();
  28. }
  29. }
  30.  
  31. package com.dxz.queue.block;
  32.  
  33. public class NativeSynchronousQueueTest {
  34.  
  35. public static void main(String[] args) throws InterruptedException {
  36. final NativeSynchronousQueue<String> queue = new NativeSynchronousQueue<String>();
  37. Thread putThread = new Thread(new Runnable() {
  38. @Override
  39. public void run() {
  40. System.out.println("put thread start");
  41. try {
  42. queue.put("1");
  43. } catch (InterruptedException e) {
  44. }
  45. System.out.println("put thread end");
  46. }
  47. });
  48.  
  49. Thread takeThread = new Thread(new Runnable() {
  50. @Override
  51. public void run() {
  52. System.out.println("take thread start");
  53. try {
  54. System.out.println("take from putThread: " + queue.take());
  55. } catch (InterruptedException e) {
  56. }
  57. System.out.println("take thread end");
  58. }
  59. });
  60.  
  61. putThread.start();
  62. Thread.sleep(1000);
  63. takeThread.start();
  64. }
  65.  
  66. }
  1. 结果:

put thread start
take thread start
put thread end
take from putThread: 1
take thread end

 

3.1.2、信号量实现

经典同步队列实现采用了三个信号量,代码很简单,比较容易理解:

  1. package com.dxz.queue.block;
  2.  
  3. import java.util.concurrent.Semaphore;
  4.  
  5. public class SemaphoreSynchronousQueue<E> {
  6. E item = null;
  7. Semaphore sync = new Semaphore(0);
  8. Semaphore send = new Semaphore(1);
  9. Semaphore recv = new Semaphore(0);
  10.  
  11. public E take() throws InterruptedException {
  12. recv.acquire();
  13. E x = item;
  14. sync.release();
  15. send.release();
  16. return x;
  17. }
  18.  
  19. public void put (E x) throws InterruptedException{
  20. send.acquire();
  21. item = x;
  22. recv.release();
  23. sync.acquire();
  24. }
  25. }
  26.  
  27. package com.dxz.queue.block;
  28.  
  29. public class SemaphoreSynchronousQueueTest {
  30.  
  31. public static void main(String[] args) throws InterruptedException {
  32. final SemaphoreSynchronousQueue<String> queue = new SemaphoreSynchronousQueue<String>();
  33. Thread putThread = new Thread(new Runnable() {
  34. @Override
  35. public void run() {
  36. System.out.println("put thread start");
  37. try {
  38. queue.put("1");
  39. } catch (InterruptedException e) {
  40. }
  41. System.out.println("put thread end");
  42. }
  43. });
  44.  
  45. Thread takeThread = new Thread(new Runnable() {
  46. @Override
  47. public void run() {
  48. System.out.println("take thread start");
  49. try {
  50. System.out.println("take from putThread: " + queue.take());
  51. } catch (InterruptedException e) {
  52. }
  53. System.out.println("take thread end");
  54. }
  55. });
  56.  
  57. putThread.start();
  58. Thread.sleep(1000);
  59. takeThread.start();
  60. }
  61.  
  62. }

  

  1. 结果:
  2. put thread start
  3. take thread start
  4. take from putThread: 1
  5. take thread end
  6. put thread end
 

在多核机器上,上面方法的同步代价仍然较高,操作系统调度器需要上千个时间片来阻塞或唤醒线程,而上面的实现即使在生产者put()时已经有一个消费者在等待的情况下,阻塞和唤醒的调用仍然需要。

3.1.3、Java 5实现

  1. package com.dxz.queue.block;
  2.  
  3. import java.util.Queue;
  4. import java.util.concurrent.locks.AbstractQueuedSynchronizer;
  5. import java.util.concurrent.locks.ReentrantLock;
  6.  
  7. public class Java5SynchronousQueue<E> {
  8. ReentrantLock qlock = new ReentrantLock();
  9. Queue waitingProducers = new Queue();
  10. Queue waitingConsumers = new Queue();
  11.  
  12. static class Node extends AbstractQueuedSynchronizer {
  13. E item;
  14. Node next;
  15.  
  16. Node(Object x) { item = x; }
  17. void waitForTake() { /* (uses AQS) */ }
  18. E waitForPut() { /* (uses AQS) */ }
  19. }
  20.  
  21. public E take() {
  22. Node node;
  23. boolean mustWait;
  24. qlock.lock();
  25. node = waitingProducers.pop();
  26. if(mustWait = (node == null))
  27. node = waitingConsumers.push(null);
  28. qlock.unlock();
  29.  
  30. if (mustWait)
  31. return node.waitForPut();
  32. else
  33. return node.item;
  34. }
  35.  
  36. public void put(E e) {
  37. Node node;
  38. boolean mustWait;
  39. qlock.lock();
  40. node = waitingConsumers.pop();
  41. if (mustWait = (node == null))
  42. node = waitingProducers.push(e);
  43. qlock.unlock();
  44.  
  45. if (mustWait)
  46. node.waitForTake();
  47. else
  48. node.item = e;
  49. }
  50. }

Java 5的实现相对来说做了一些优化,只使用了一个锁,使用队列代替信号量也可以允许发布者直接发布数据,而不是要首先从阻塞在信号量处被唤醒。

3.1.4、Java6实现

Java 6的SynchronousQueue的实现采用了一种性能更好的无锁算法 — 扩展的“Dual stack and Dual queue”算法。性能比Java5的实现有较大提升。竞争机制支持公平和非公平两种:非公平竞争模式使用的数据结构是后进先出栈(Lifo Stack);公平竞争模式则使用先进先出队列(Fifo Queue),性能上两者是相当的,一般情况下,Fifo通常可以支持更大的吞吐量,但Lifo可以更大程度的保持线程的本地化。

代码实现里的Dual Queue或Stack内部是用链表(LinkedList)来实现的,其节点状态为以下三种情况:

  1. 持有数据 – put()方法的元素
  2. 持有请求 – take()方法

这个算法的特点就是任何操作都可以根据节点的状态判断执行,而不需要用到锁。

其核心接口是Transfer,生产者的put或消费者的take都使用这个接口,根据第一个参数来区别是入列(栈)还是出列(栈)。

  1. /**
  2. * Shared internal API for dual stacks and queues.
  3. */
  4. static abstract class Transferer {
  5. /**
  6. * Performs a put or take.
  7. *
  8. * @param e if non-null, the item to be handed to a consumer;
  9. * if null, requests that transfer return an item
  10. * offered by producer.
  11. * @param timed if this operation should timeout
  12. * @param nanos the timeout, in nanoseconds
  13. * @return if non-null, the item provided or received; if null,
  14. * the operation failed due to timeout or interrupt --
  15. * the caller can distinguish which of these occurred
  16. * by checking Thread.interrupted.
  17. */
  18. abstract Object transfer(Object e, boolean timed, long nanos);
  19. }

TransferQueue实现如下(摘自Java 6源代码),入列和出列都基于Spin和CAS方法:

  1. /**
  2. * Puts or takes an item.
  3. */
  4. Object transfer(Object e, boolean timed, long nanos) {
  5. /* Basic algorithm is to loop trying to take either of
  6. * two actions:
  7. *
  8. * 1. If queue apparently empty or holding same-mode nodes,
  9. * try to add node to queue of waiters, wait to be
  10. * fulfilled (or cancelled) and return matching item.
  11. *
  12. * 2. If queue apparently contains waiting items, and this
  13. * call is of complementary mode, try to fulfill by CAS'ing
  14. * item field of waiting node and dequeuing it, and then
  15. * returning matching item.
  16. *
  17. * In each case, along the way, check for and try to help
  18. * advance head and tail on behalf of other stalled/slow
  19. * threads.
  20. *
  21. * The loop starts off with a null check guarding against
  22. * seeing uninitialized head or tail values. This never
  23. * happens in current SynchronousQueue, but could if
  24. * callers held non-volatile/final ref to the
  25. * transferer. The check is here anyway because it places
  26. * null checks at top of loop, which is usually faster
  27. * than having them implicitly interspersed.
  28. */
  29.  
  30. QNode s = null; // constructed/reused as needed
  31. boolean isData = (e != null);
  32.  
  33. for (;;) {
  34. QNode t = tail;
  35. QNode h = head;
  36. if (t == null || h == null) // saw uninitialized value
  37. continue; // spin
  38.  
  39. if (h == t || t.isData == isData) { // empty or same-mode
  40. QNode tn = t.next;
  41. if (t != tail) // inconsistent read
  42. continue;
  43. if (tn != null) { // lagging tail
  44. advanceTail(t, tn);
  45. continue;
  46. }
  47. if (timed &amp;&amp; nanos &lt;= 0) // can't wait
  48. return null;
  49. if (s == null)
  50. s = new QNode(e, isData);
  51. if (!t.casNext(null, s)) // failed to link in
  52. continue;
  53.  
  54. advanceTail(t, s); // swing tail and wait
  55. Object x = awaitFulfill(s, e, timed, nanos);
  56. if (x == s) { // wait was cancelled
  57. clean(t, s);
  58. return null;
  59. }
  60.  
  61. if (!s.isOffList()) { // not already unlinked
  62. advanceHead(t, s); // unlink if head
  63. if (x != null) // and forget fields
  64. s.item = s;
  65. s.waiter = null;
  66. }
  67. return (x != null)? x : e;
  68.  
  69. } else { // complementary-mode
  70. QNode m = h.next; // node to fulfill
  71. if (t != tail || m == null || h != head)
  72. continue; // inconsistent read
  73.  
  74. Object x = m.item;
  75. if (isData == (x != null) || // m already fulfilled
  76. x == m || // m cancelled
  77. !m.casItem(x, e)) { // lost CAS
  78. advanceHead(h, m); // dequeue and retry
  79. continue;
  80. }
  81.  
  82. advanceHead(h, m); // successfully fulfilled
  83. LockSupport.unpark(m.waiter);
  84. return (x != null)? x : e;
  85. }
  86. }
  87. }

3.2、SynchronousQueue实现原理

  不像ArrayBlockingQueue、LinkedBlockingDeque之类的阻塞队列依赖AQS实现并发操作,SynchronousQueue直接使用CAS实现线程的安全访问。由于源码中充斥着大量的CAS代码,不易于理解,所以按照笔者的风格,接下来会使用简单的示例来描述背后的实现模型。

队列的实现策略通常分为公平模式和非公平模式,接下来将分别进行说明。

3.2.1、公平模式下的模型:

  公平模式下,底层实现使用的是TransferQueue这个内部队列,它有一个head和tail指针,用于指向当前正在等待匹配的线程节点。
初始化时,TransferQueue的状态如下:

接着我们进行一些操作:

1、线程put1执行 put(1)操作,由于当前没有配对的消费线程,所以put1线程入队列,自旋一小会后睡眠等待,这时队列状态如下:

2、接着,线程put2执行了put(2)操作,跟前面一样,put2线程入队列,自旋一小会后睡眠等待,这时队列状态如下:

3、这时候,来了一个线程take1,执行了 take操作,由于tail指向put2线程,put2线程跟take1线程配对了(一put一take),这时take1线程不需要入队,但是请注意了,这时候,要唤醒的线程并不是put2,而是put1。为何? 大家应该知道我们现在讲的是公平策略,所谓公平就是谁先入队了,谁就优先被唤醒,我们的例子明显是put1应该优先被唤醒。至于读者可能会有一个疑问,明明是take1线程跟put2线程匹配上了,结果是put1线程被唤醒消费,怎么确保take1线程一定可以和次首节点(head.next)也是匹配的呢?其实大家可以拿个纸画一画,就会发现真的就是这样的。
公平策略总结下来就是:队尾匹配队头出队。
执行后put1线程被唤醒,take1线程的 take()方法返回了1(put1线程的数据),这样就实现了线程间的一对一通信,这时候内部状态如下:

4、最后,再来一个线程take2,执行take操作,这时候只有put2线程在等候,而且两个线程匹配上了,线程put2被唤醒,
take2线程take操作返回了2(线程put2的数据),这时候队列又回到了起点,如下所示:

以上便是公平模式下,SynchronousQueue的实现模型。总结下来就是:队尾匹配队头出队,先进先出,体现公平原则。

非公平模式下的模型:

我们还是使用跟公平模式下一样的操作流程,对比两种策略下有何不同。非公平模式底层的实现使用的是TransferStack,
一个栈,实现中用head指针指向栈顶,接着我们看看它的实现模型:

1、线程put1执行 put(1)操作,由于当前没有配对的消费线程,所以put1线程入栈,自旋一小会后睡眠等待,这时栈状态如下:

2、接着,线程put2再次执行了put(2)操作,跟前面一样,put2线程入栈,自旋一小会后睡眠等待,这时栈状态如下:

3、这时候,来了一个线程take1,执行了take操作,这时候发现栈顶为put2线程,匹配成功,但是实现会先把take1线程入栈,然后take1线程循环执行匹配put2线程逻辑,一旦发现没有并发冲突,就会把栈顶指针直接指向 put1线程

4、最后,再来一个线程take2,执行take操作,这跟步骤3的逻辑基本是一致的,take2线程入栈,然后在循环中匹配put1线程,最终全部匹配完毕,栈变为空,恢复初始状态,如下图所示:

可以从上面流程看出,虽然put1线程先入栈了,但是却是后匹配,这就是非公平的由来。

总结

SynchronousQueue由于其独有的线程一一配对通信机制,在大部分平常开发中,可能都不太会用到,但线程池技术中会有所使用,由于内部没有使用AQS,而是直接使用CAS,所以代码理解起来会比较困难,但这并不妨碍我们理解底层的实现模型,在理解了模型的基础上,有兴趣的话再查阅源码,就会有方向感,看起来也会比较容易,希望本文有所借鉴意义。

转自:Java并发包中的同步队列SynchronousQueue实现原理

【转载】阻塞队列之三:SynchronousQueue同步队列 阻塞算法的3种实现的更多相关文章

  1. 阻塞队列之三:SynchronousQueue同步队列 阻塞算法的3种实现

    一.SynchronousQueue简介 Java 6的并发编程包中的SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除 ...

  2. 【转载】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)

    原文链接:https://www.cnblogs.com/lixinjie/p/10811219.html 常规的误区 假设有一个展示用户详情的需求,分两步,先调用一个HTTP接口拿到详情数据,然后使 ...

  3. AbstractQueuedSynchronizer同步队列与Condition等待队列协同机制

    概要: AQS维护了一个同步队列 Condition是JUC的一个接口,AQS的ConditionObject实现了这个接口,维护了一个等待队列(等待signal信号的队列) 线程调用reentran ...

  4. CLH同步队列

    原文链接:https://blog.csdn.net/chenssy/article/details/60781148 AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一 ...

  5. J.U.C之AQS:CLH同步队列

    此篇博客所有源码均来自JDK 1.8 在上篇博客[死磕Java并发]—–J.U.C之AQS:AQS简介中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个FIFO ...

  6. 【死磕Java并发】-----J.U.C之AQS:CLH同步队列

    此篇博客全部源代码均来自JDK 1.8 在上篇博客[死磕Java并发]-–J.U.C之AQS:AQS简单介绍中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个F ...

  7. 【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)

    常规的误区 假设有一个展示用户详情的需求,分两步,先调用一个HTTP接口拿到详情数据,然后使用适合的视图展示详情数据. 如果网速很慢,代码发起一个HTTP请求后,就卡住不动了,直到十几秒后才拿到HTT ...

  8. 同步/异步/阻塞/非阻塞/BIO/NIO/AIO

    转摘自:https://www.cnblogs.com/lixinjie/p/a-post-about-io-clearly.html 常规的误区 假设有一个展示用户详情的需求,分两步,先调用一个HT ...

  9. IO同步阻塞与同步非阻塞

    BIO.NIO.AIO IO(BIO)和NIO区别:其本质就是阻塞和非阻塞的区别 阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,就会一直等待,直到传输完毕为止. 非阻塞概念:应用程序直 ...

随机推荐

  1. libpng warning: iCCP: known incorrect sRGB profile

    参考  http://www.cocos2d-x.org/forums/6/topics/49093 解决 I got the following warnings in console when r ...

  2. [ISSUE] [Centos] Centos Start Nginx Show: Failed to start nginx.service:unit not found

    CMD Line:systemctl start nginx.serviceFailed to start nginx.service: Unit not found. Solution: 1.vim ...

  3. you've successfully authenticated, but Gitee.com does not provide she access.

    如果都是正常的生成ssh的操作,还是会报这个错误,那么就是.... 你没更改文件夹的权限,这个坑跳了很久(汗...) sudo chmod 777 -r 文件夹

  4. ubuntu上传到百度网盘

    1 2 亲测可以上传

  5. Java-线程间通信小结

    1)方法wait的作用是使当前执行代码的线程进行等待,将当前线程置入预执行队列,并且在wait所在代码行处停止执行,直到接到通知或者中断.在wait之前,要获得一个对象锁,即wait只能在同步方法/块 ...

  6. pyCharm最新2018激活码

    本教程对jetbrains全系列可用例:IDEA.WebStorm.phpstorm.clion等 因公司的需求,需要做一个爬取最近上映的电影.列车号.航班号.机场.车站等信息,所以需要我做一个爬虫项 ...

  7. Spring:容器基本用法

    bean是Spring 最核心的东西,打个比方,假设Spring是一个水桶,那么bean就是水桶里的水,水桶离开水后,就没啥作用了.我们先来看一下bean的定义: public class Perso ...

  8. 移动端js调试工具:eruda

    通常写前端页面都在Chrome浏览器的开发模式下进行调试,但是写放在移动端的H5页面时,有时候会遇到在Chrome上调试没有问题,但是在手机的浏览器上有问题的情况:或者有些功能只能在特定的容器中才能其 ...

  9. Hi3516A开发--接触(转)

    Hi3516A开发--接触

  10. php 加密解密算法 用于数据传输

    /** * 加密方法 * @param string $data 要加密的字符串 * @param string $key 加密密钥 * @param int $expire 过期时间 (单位:秒) ...