• Java并发包之同步队列SynchronousQueue理解


    1 简介

    SynchronousQueue是这样一种阻塞队列,其中每个put必须等待一个take,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。不能在同步队列上进行peek,因为仅在试图要取得元素时,该元素才存在,除非另一个线程试图移除某个元素,否则也不能(使用任何方法)添加元素,也不能迭代队列,因为其中没有元素可用于迭代。队列的头是尝试添加到队列中的首个已排队线程元素,如果没有已排队线程,则不添加元素并且头为 null。
    对于其他Collection方法(例如 contains),SynchronousQueue作为一个空集合,此队列不允许 null 元素。
    同步队列类似于CSP和Ada中使用的rendezvous信道。它非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。
    对于正在等待的生产者和使用者线程而言,此类支持可选的公平排序策略,默认情况下不保证这种排序。
    但是,使用公平设置为true所构造的队列可保证线程以FIFO的顺序进行访问。公平通常会降低吞吐量,但是可以减小可变性并避免得不到服务。

    2 使用示例

      1 import static org.junit.Assert.assertEquals;
      2 
      3 import java.util.concurrent.CountDownLatch;
      4 import java.util.concurrent.ExecutorService;
      5 import java.util.concurrent.Executors;
      6 import java.util.concurrent.SynchronousQueue;
      7 import java.util.concurrent.ThreadLocalRandom;
      8 import java.util.concurrent.TimeUnit;
      9 import java.util.concurrent.atomic.AtomicInteger;
     10 
     11 import org.junit.Test;
     12 
     13 /**
     14  * synchronousqueue的使用场景  ==== 线程间共享元素
     15  * 假设有两个线程,一个生产者和一个消费者,当生产者设置一个共享变量的值时,我们希望向消费者线程
     16  * 发出这个信号,然后消费者线程将从共享变量取值。
     17  * @author ko
     18  *
     19  */
     20 public class Sqt {
     21 
     22     /**
     23      * 利用AtomicInteger+CountDownLatch实现
     24      */
     25     @Test
     26     public void doingByCountDownLatch(){
     27         ExecutorService executor = Executors.newFixedThreadPool(2);
     28         AtomicInteger sharedState = new AtomicInteger();// 共享变量值
     29         //  协调这两个线程,以防止情况当消费者访问共享变量值
     30         CountDownLatch countDownLatch = new CountDownLatch(1);
     31         
     32         // 生产商将设置一个随机整数到sharedstate变量,并countdown()方法, 
     33         // 信号给消费者,它可以从sharedstate取一个值
     34         Runnable producer = () -> {// 这好像是java8的匿名内部类的新写法
     35             Integer producedElement = ThreadLocalRandom.current().nextInt();
     36             sharedState.set(producedElement);
     37             System.out.println("生产者给变量设值:"+producedElement);
     38             countDownLatch.countDown();
     39         };
     40         
     41         // 消费者会等待countdownlatch执行到await()方法,获取许可后,再从生产者里获取变量sharedstate值
     42         Runnable consumer = () -> {
     43             try {
     44                 countDownLatch.await();
     45                 Integer consumedElement = sharedState.get();
     46                 System.out.println("消费者获取到变量:"+consumedElement);
     47             } catch (InterruptedException ex) {
     48                 ex.printStackTrace();
     49             }
     50         };
     51         
     52         executor.execute(producer);
     53         executor.execute(consumer);
     54         try {
     55             executor.awaitTermination(500, TimeUnit.MILLISECONDS);
     56         } catch (InterruptedException e) {
     57             e.printStackTrace();
     58         }
     59         executor.shutdown();
     60         assertEquals(countDownLatch.getCount(), 0);
     61     }
     62     
     63     /**
     64      * 仅使用SynchronousQueue就可以实现
     65      */
     66     @Test
     67     public void doingBySynchronousQueue(){
     68         ExecutorService executor = Executors.newFixedThreadPool(2);
     69         SynchronousQueue<Integer> queue = new SynchronousQueue<>();
     70         
     71         // 生产者
     72         Runnable producer = () -> {
     73             Integer producedElement = ThreadLocalRandom.current().nextInt();
     74             try {
     75                 queue.put(producedElement);
     76                 System.out.println("生产者设值:"+producedElement);
     77             } catch (InterruptedException ex) {
     78                 ex.printStackTrace();
     79             }
     80         };
     81         
     82         // 消费者
     83         Runnable consumer = () -> {
     84             try {
     85                 Integer consumedElement = queue.take();
     86                 System.out.println("消费者取值:"+consumedElement);
     87             } catch (InterruptedException ex) {
     88                 ex.printStackTrace();
     89             }
     90         };
     91         
     92         executor.execute(producer);
     93         executor.execute(consumer);
     94         try {
     95             executor.awaitTermination(500, TimeUnit.MILLISECONDS);
     96         } catch (InterruptedException e) {
     97             e.printStackTrace();
     98         }
     99         executor.shutdown();
    100         assertEquals(queue.size(), 0);
    101     }
    102 }

    3 实现原理

    阻塞队列的实现方法有许多。

    3.1 阻塞算法实现

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

     1 public class NativeSynchronousQueue<E> {
     2     boolean putting = false;
     3     E item = null;
     4 
     5     public synchronized E take() throws InterruptedException {
     6         while (item == null)
     7             wait();
     8         E e = item;
     9         item = null;
    10         notifyAll();
    11         return e;
    12     }
    13 
    14     public synchronized void put(E e) throws InterruptedException {
    15         if (e==null) return;
    16         while (putting)
    17             wait();
    18         putting = true;
    19         item = e;
    20         notifyAll();
    21         while (item!=null)
    22             wait();
    23         putting = false;
    24         notifyAll();
    25     }
    26 }

    3.2 信号量实现

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

     1 public class SemaphoreSynchronousQueue<E> {
     2     E item = null;
     3     Semaphore sync = new Semaphore(0);
     4     Semaphore send = new Semaphore(1);
     5     Semaphore recv = new Semaphore(0);
     6 
     7     public E take() throws InterruptedException {
     8         recv.acquire();
     9         E x = item;
    10         sync.release();
    11         send.release();
    12         return x;
    13     }
    14 
    15     public void put (E x) throws InterruptedException{
    16         send.acquire();
    17         item = x;
    18         recv.release();
    19         sync.acquire();
    20     }
    21 }

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

    3.3 Java 5实现

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

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

    3.4 Java 6实现

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

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

    持有数据 – put()方法的元素

    持有请求 – 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    }
  • 相关阅读:
    greenplum日常维护手册
    Android UI界面基本属性 大全
    Listview 选项按下去黑了所有按钮的解决方法 ——android:cacheColorHint=“#00000000”
    【转】Android应用程序模块详解
    android退出有多个activity的应用
    启动模式"singleTask"和FLAG_ACTIVITY_NEW_TASK具有不同的行为!
    Android 按两次back键退出 效率最高版
    【转】跑马灯效果
    Sundy笔记__Git版本控制
    如果你想用对话框代替一个activity的话,可以设置activity的主题属性
  • 原文地址:https://www.cnblogs.com/shamo89/p/7363339.html
Copyright © 2020-2023  润新知