音視頻開發之旅(55)-阻塞隊列與無鎖併發容器

目錄

  1. 阻塞隊列的定義和使用場景
  2. 阻塞的隊列的實現原理
  3. 簡單學習無鎖併發容器之ConcurrentLinkedQueue和CAS
  4. 資料
  5. 收穫

一、阻塞隊列的定義和使用場景

阻塞隊列(BlockingQueue)在隊列Queue的基礎上增加了兩個場景的阻塞

  1. 當隊列滿時,再向隊列添加數據會阻塞,直到隊列不滿時
  2. 當隊列爲空時,再向隊列獲取數據會阻塞,直到隊列變爲非空

阻塞隊列常用於生產者消費者的場景

下面我們先來Queue和BolckingQueue接口的定義

//java.util.Queue
public interface Queue<E> extends Collection<E> {

  //添加一個元素到隊列,如果隊列滿時會拋出異常IllegalStateException
  boolean add(E e); 

  //添加一個元素到隊列,如果隊列滿時不會拋異常,而是返回false
  boolean offer(E e);
  
  //從隊列中獲取並移除一個元素,如果隊列爲空, 會拋出NoSuchElementException
  E remove();

  //從隊列中獲取並移除一個元素,如果隊列爲空, 不會拋異常,而是返回null
  E poll();
  
  //從隊列中獲取一個元素 但不移除。注意和remove的區別
  //當隊列爲空時,會拋出異常NoSuchElementException
  E element();
  
  //從隊列中獲取一個元素,也不移除。注意和poll的區別
  //當隊列爲空時,不會拋出異常,而是返回null
  E peek();
}
//java.util.concurrent.BlockingQueue
public interface BlockingQueue<E> extends Queue<E> {


  //插入一個元素到隊列,如果隊列滿了,等待直到有空間空用
  void put(E e) throws InterruptedException;

  //插入一個元素到隊列,如果隊列滿了,等待一定時間返回,或者有空間空用
  boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

  //獲取隊列的頭元素,如果隊列爲空,則等待
  E take() throws InterruptedException;

  //從隊列中獲取並移除一個元素,如果隊列爲空,等待一段時間
  E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

}

我們可以看到BlockingQueue繼承自Queue並且新增了幾個阻塞的方法。

Java中BlockingQueue接口有七個實現類,分別如下:

  1. ArrayBlockingQueue : 由數組結構組成的有界阻塞隊列,在添加和獲取時內部使用一個ReentrantLock可重入同步鎖
  2. LinkedBlockingQueue:由鏈表結構組成的有界阻塞隊列。在添加和獲取時內部使用兩個ReentrantLock,吞吐量高於ArrayBlockingQueue,Executors#newSingleThreadExecutor()和Executors#newFixedThreadPool(int)都使用了這個阻塞隊列
  public static ExecutorService newSingleThreadExecutor() {
           return new FinalizableDelegatedExecutorService
               (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
       }
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  1. SynchronousQueue:不存儲元素的阻塞隊列。每個插入操作必須等待另個一個線程調用的移除操作,否則一致處於阻塞狀態。吞吐量一般高於LinkedBlockingQueue。Executors#newCachedThreadPool()使用了這個阻塞隊列
 public static ExecutorService newCachedThreadPool() {
           return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                         60L, TimeUnit.SECONDS,
                                         new SynchronousQueue<Runnable>());
       }
  1. PriorityBlockingQueue:支持優先級排序的無界阻塞隊列
  2. DelayQueue:使用優先級隊列實現的支持延遲獲取元素的無界阻塞隊列
  3. TransferQueue:鏈表結構組成的無界阻塞隊列
  4. BlockingDeque:鏈表結構組成的雙向阻塞隊列

二、阻塞的隊列的實現原理(LinkedBlockingQueue)

我們以LinkedBlockingQueue來分析

 //節點結構體 
  static class Node<E> {
        E item;
        Node<E> next;

        Node(E x) { item = x; }
    }


    /** 從隊列獲取元素時的可重入鎖 ,非公平鎖*/
    private final ReentrantLock takeLock = new ReentrantLock();

    /** 非空condition,等待隊列非空*/
    private final Condition notEmpty = takeLock.newCondition();

    /** 向隊列中插入元素時的可重入鎖 ,非公平鎖*/
    private final ReentrantLock putLock = new ReentrantLock();

    /** 非滿condition,等待隊列非滿 */
    private final Condition notFull = putLock.newCondition();

    /**
     * 當隊列有元素後,發出非空信號
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

    /**
     * 當隊列由滿到不滿後,發出該非滿信號
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

2.1 插入元素到隊列

offer的實現 (添加一個元素到隊列,如果隊列滿時不會拋異常,而是返回false)

 public boolean offer(E e) {
        ...
        int c = -1;
        Node<E> node = new Node<E>(e);
        //獲取寫 可重入鎖
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            //如果隊列還未滿,插入該元素節點
            if (count.get() < capacity) {
                // enqueue 插入元素到隊列,一會我們在看下其實現
                enqueue(node);
                c = count.getAndIncrement();
                //如果插入後,還隊列還未滿,發送未滿信號
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        // 如果成功插入後,發送非空信號
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }

put的實現 (插入一個元素到隊列,如果隊列滿了,等待直到有空間空用)

public void put(E e) throws InterruptedException {
        ...
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
  
        //相比較offer這是差異點1
        //採用了可中斷鎖,等待過程中可以接收中斷
        putLock.lockInterruptibly();
        try {
           //相比較offer這是差異點2,
           //如果當前隊列滿了,則阻塞,等待非空的信號到來
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

enqueue的實現

 private void enqueue(Node<E> node) {
       //把當前節點作爲隊列先前未節點的next插入到隊列中
       //然後吧last指向新插入的節點
        last = last.next = node;
    }

2.2 從隊列獲取元素

poll的實現(從隊列中獲取並移除一個元素,如果隊列爲空, 不會拋異常,而是返回null)

public E poll() {
       ...
        int c = -1;
        //獲取取 可重入鎖
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
             //如果當前隊列的元素個數大於0
            if (count.get() > 0) {
                //dequeue 從隊列中獲取一個元素,稍後再分析
                x = dequeue();
               //取出後如果隊列中元素的個數還大於1
              //(爲什麼不是大於0? 
              //    這是因爲getAndDecrement的實現是先獲取再減1),
              // 則發出非空信號
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        //如果c的值等於容器的值(由於getAndDecrement的實現是先獲取再減1,這是隊列從滿變爲了非滿狀態),則發出非滿信號
        if (c == capacity)
            signalNotFull();
        return x;
    }

take的實現 (獲取隊列的頭元素,如果隊列爲空,則等待)

 public E take() throws InterruptedException {

        ...
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        //和poll的差異點1:wait時支持中斷
        takeLock.lockInterruptibly();
        try {
          //和poll的差異點2:如果隊列爲空,則阻塞等待,知道收到非空的信號
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

dequeue的實現

    private E dequeue() {
        //鏈表操作的通用做法,head是一個虛節點
        Node<E> h = head;
        //頭節點的next賦值給定義的first節點
        Node<E> first = h.next;
       //把先前的頭節點頭的next指向自身節點,方便gc
        h.next = h; // help GC
        //標記新的頭節點給到head指針
        head = first;
        //獲取元素
        E x = first.item;
        first.item = null;
        return x;
    }

爲了方便dequeue的理解,畫下列表的節點圖如下


我們看先LinkedBlockingQueue再線程池中的使用,前面已經提到了,Executors#newSingleThreadExecutor()和Executors#newFixedThreadPool(int)都使用了LinkedBlockingQueue,我們通過下面兩張來自《java併發編程的藝術》的示意圖來看下


其他阻塞隊列的實現可以自行分析下,比如ArrayBlockingQueue和SynchronousQueue的實現。

三、簡單學習無鎖併發容器之ConcurrentLinkedQueue和CAS

上面介紹的LinkedBlockingQueue通過加鎖阻塞的方式保證線程安全性。還有一種非阻塞的算法實現。ConcurrentLinkedQueue就是通過後者實現的,我們一起來分析學習下。

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {


    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;
    }

    static <E> Node<E> newNode(E item) {
        Node<E> node = new Node<E>();
        //這裏的U是sun.misc.Unsafe
        U.putObject(node, ITEM, item);
        return node;
    }

    static <E> boolean casNext(Node<E> node, Node<E> cmp, Node<E> val) {
        return U.compareAndSwapObject(node, NEXT, cmp, val);
    }


public boolean offer(E e) {
        final Node<E> newNode = newNode(Objects.requireNonNull(e));

        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                // p is last node
                if (casNext(p, null, newNode)) {
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
                // Lost CAS race to another thread; re-read next
            }
            else if (p == q)
                p = (t != (t = tail)) ? t : head;
            else
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

}

Unsafe類
Unsafe類中存在直接操作內存的方法 ,Java中CAS操作的執行依賴於Unsafe類的方法,注意Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接調用操作系統底層資源執行相應任務

CAS爲什麼能保證原子性?
無鎖策略則採用一種稱爲CAS的技術來保證線程執行的安全性
CAS的全稱是Compare And Swap 即比較交換,其算法核心思想如下

CAS(V,E,N)

其包含3個參數
V表示要更新的變量
E表示預期值
N表示新值
//如果V值等於E值,則將V的值設爲N。若V值和E值不同,則說明已經有其他線程做了更新,則當前線程什麼都不做

假設存在多個線程執行CAS操作並且CAS的步驟很多,有沒有可能在判斷V和E相同後,正要賦值時,切換了線程,更改了值。造成了數據不一致呢?

答案是否定的,因爲CAS是一種系統原語,原語屬於操作系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致問題。

Unsafe這塊源碼解析和理解還是有些不足,根據需要再去看吧,Java併發系列到這裏就暫時告一段落。
接下來進入編解碼的學習時間,準備建立學習和寫作打卡羣,有興趣的歡迎加我微信“yabin_yangO2O”,備註 視頻編碼讀書寫作
,一起學習成長。

四、資料

  1. 圖書:《Java併發編程的藝術》
  2. 深入剖析java併發之阻塞隊列LinkedBlockingQueue與ArrayBlockingQueue
  3. Java併發編程-無鎖CAS與Unsafe類及其併發包Atomic

五、收穫

通過本篇的學習實踐

  1. 分析了java併發阻塞隊列的應用和實現
  2. 簡單分析學習了CAS和無鎖併發容器ConcurrentLinkedQueue

感謝你的閱讀,Java併發編程到這裏就暫告一段落,接下來一段時間會進入編碼的學習時間。
主要是針對《視頻編碼全角度詳解》這本書的閱讀和實踐。以21天爲一個週期(不一定要讀完,但是每天至少讀一頁,且至少輸出50字),有興趣的朋友可以一起來學習交流,加我微信“yabin_yangO2O”,備註 視頻編碼讀書寫作

下一篇我們開始視頻編碼知識的學習實踐,歡迎關注公衆號“音視頻開發之旅”,一起學習成長。

歡迎交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章