從原始碼解析-Android資料結構之單向阻塞佇列LinkedBlockingQueue的使用

硬剛平底鍋發表於2018-06-23

前言

在一篇分析AsyncTask原始碼中的文章中,我們看到了線上程併發中用到比較多的一個佇列LinkedBlockingQueue,今天這篇文章就來分析下這個東西的使用

相關文章

從原始碼解析-Android資料結構ArrayBlockingQueue
從原始碼解析-Android資料結構之雙向阻塞佇列LinkedBlockingDeque的使用
從原始碼解析-Android資料結構之單向阻塞佇列LinkedBlockingQueue的使用
從原始碼解析-Android資料結構之雙端佇列ArrayDeque

原理圖

LinkedBlockingQueue:是concurrent包下的類,實現了BlockingQueue介面,是一種單向阻塞連結串列,是執行緒安全的佇列,資料處理邏輯是先進先出,應該說是作為生產者消費者模型的首選;可以指定最大容量,也可以不指定最大容量,如果不指定的話,預設最大值是Integer.MAX_VALUE。如圖

這裡寫圖片描述

執行緒1不停的往佇列尾部加入資料,執行緒2不停的從頭部取出資料。

通過使用這種佇列,我們可以放心的實現多執行緒操作,不需要自己開發,要處理各種物件鎖,佇列安全問題。

增加刪除方法

LingkedBlockingQueue各有一套增加刪除資料的方法

 *  <tr>
 *  <tr>
 *    <td><b>Insert</b></td>
 *    <td>{@link #add add(e)}</td>
 *    <td>{@link #offer offer(e)}</td>
 *    <td>{@link #put put(e)}</td>
 *    <td>{@link #offer(Object, long, TimeUnit) offer(e, time, unit)}</td>
 *  </tr>
 *  <tr>
 *    <td><b>Remove</b></td>
 *    <td>{@link #remove remove()}</td>
 *    <td>{@link #poll poll()}</td>
 *    <td>{@link #take take()}</td>
 *    <td>{@link #poll(long, TimeUnit) poll(time, unit)}</td>
 *  </t>

使用樣例

現在我們通過程式碼來看看這兩套方法如何操作。本例使用生產者消費者模型,構建一個蘋果消費者,一個蘋果生產者,一個裝蘋果的籃子,一個蘋果物件。

蘋果生產者

/**
 * @Description TODO(蘋果生產者)
 * @author cxy
 * @Date 2018/6/21 17:13
 */
public class Producer implements Runnable{

    private String TAG = "Producer";

    //生產者
    private String produce;
    //蘋果籃子
    private AppleBasket basket;

    public Producer(String produce, AppleBasket basket) {
        this.produce = produce;
        this.basket = basket;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; ; i++) {
                Apple bean = new Apple("蘋果" + i + "號");
                Log.e(TAG, produce + "正在生產蘋果 " + bean);
                basket.produce(bean, produce);
                Log.e(TAG, produce + "生產蘋果 " + bean + " 結束----------------");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            Log.e(TAG, "InterruptedException=" + e.getMessage());
            e.printStackTrace();
        } catch (IllegalStateException e) {
            Log.e(TAG, "IllegalStateException=" + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            Log.e(TAG, "Exception=" + e.getMessage());
            e.printStackTrace();
        }
    }
}

蘋果消費者

/**
 * @Description TODO(蘋果消費者)
 * @author cxy
 * @Date 2018/6/21 17:14
 */
public class Consumer implements Runnable {

    private String TAG = "Consumer";

    private String consume;
    private AppleBasket basket;

    public Consumer(String consume, AppleBasket basket) {
        this.consume = consume;
        this.basket = basket;
    }

    @Override
    public void run() {

        try {
            while (true) {
                Log.e(TAG,consume+"正在消費蘋果 ");
                basket.consume(consume);
                Log.e(TAG,consume+"消費蘋果結束 -----------------------");
                Thread.sleep(1000 * 20);
            }
        } catch (InterruptedException e) {
            Log.e(TAG, "InterruptedException=" + e.getMessage());
            e.printStackTrace();
        } catch (IllegalStateException e) {
            Log.e(TAG, "IllegalStateException=" + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            Log.e(TAG, "Exception=" + e.getMessage());
            e.printStackTrace();
        }

    }


}

蘋果籃子

/**
 * @Description TODO(裝蘋果的籃子)
 * @author cxy
 * @Date 2018/6/21 17:03
 */
public class AppleBasket {

    private String TAG = "AppleBasket";

    BlockingQueue<Apple> queue = new LinkedBlockingQueue<>(10);

    /**
     * 生成蘋果,放入佇列
     * @param bean
     * @throws InterruptedException
     */
    public void produce(Apple bean,String producer) throws InterruptedException {
        //新增元素到佇列,如果佇列已滿,執行緒進入等待,直到有空間繼續生產
        queue.put(bean);
        //新增元素到佇列,如果佇列已滿,丟擲IllegalStateException異常,退出生產模式
//        queue.add(bean);
        //新增元素到佇列,如果佇列已滿或者說新增失敗,返回false,否則返回true,繼續生產
//        queue.offer(bean);
        //新增元素到佇列,如果佇列已滿,就等待指定時間,如果新增成功就返回true,否則false,繼續生產
//        queue.offer(bean,5, TimeUnit.SECONDS);
    }

    /**
     * 消費蘋果。從佇列取出
     * @return
     * @throws InterruptedException
     */
    public Apple consume(String consumer) throws InterruptedException {
        //檢索並移除佇列頭部元素,如果佇列為空,執行緒進入等待,直到有新的資料加入繼續消費
        Apple bean = queue.take();
        //檢索並刪除佇列頭部元素,如果佇列為空,丟擲異常,退出消費模式
//        Apple bean = queue.remove();
        //檢索並刪除佇列頭部元素,如果佇列為空,返回false,否則返回true,繼續消費
//        Apple bean = queue.poll();
        //檢索並刪除佇列頭部元素,如果佇列為空,則等待指定時間,成功返回true,否則返回false,繼續消費
//        Apple bean = queue.poll(3, TimeUnit.SECONDS);
        return bean;
    }
}

蘋果物件

/**
 * @Description TODO(蘋果類)
 * @author cxy
 * @Date 2018/6/21 17:04
 */
public class Apple {

    private String name;

    public Apple(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return  name ;
    }
}

現在來使用吧,在Activity裡使用

 AppleBasket basket = new AppleBasket();
        ExecutorService service = Executors.newFixedThreadPool(5);
        Producer producer = new Producer("生產者",basket);
        Consumer consumer = new Consumer("消費者",basket);
        service.execute(producer);
        service.execute(consumer);

上面程式碼邏輯就是生產者每過一秒生產一個蘋果,籃子採用put方法往佇列裡放蘋果;消費者每過20秒消費一個蘋果,籃子採用take方法從籃子取資料,佇列的容量是隻能放10個蘋果;這樣的情況就是當生產者生產了10個蘋果以後,籃子就滿了,但是此時消費者還沒有開始消費蘋果,那麼這時候生產者就會阻塞住,也就是生產執行緒被wait了,一直到20s後消費者開始消費蘋果了,這時候生產執行緒就被喚醒了,就繼續開始生產蘋果,知道佇列滿了再繼續被阻塞。我們看列印的日誌

06-22 10:39:46.778 25997-26045/? E/Producer: 生產者正在生產蘋果 蘋果0號
06-22 10:39:46.778 25997-26045/? E/Producer: 生產者生產蘋果 蘋果0號 結束----------------
06-22 10:39:47.778 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果1號
06-22 10:39:47.779 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果1號 結束----------------
06-22 10:39:48.779 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果2號
06-22 10:39:48.779 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果2號 結束----------------
06-22 10:39:49.779 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果3號
06-22 10:39:49.779 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果3號 結束----------------
06-22 10:39:50.779 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果4號
06-22 10:39:50.780 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果4號 結束----------------
06-22 10:39:51.780 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果5號
06-22 10:39:51.780 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果5號 結束----------------
06-22 10:39:52.780 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果6號
06-22 10:39:52.780 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果6號 結束----------------
06-22 10:39:53.781 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果7號
06-22 10:39:53.781 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果7號 結束----------------
06-22 10:39:54.781 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果8號
06-22 10:39:54.781 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果8號 結束----------------
06-22 10:39:55.781 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果9號
06-22 10:39:55.782 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果9號 結束----------------
06-22 10:39:56.782 25997-26045/com.android.mangodialog E/Producer: 生產者正在生產蘋果 蘋果10號
06-22 10:40:06.778 25997-26046/com.android.mangodialog E/Consumer: 消費者正在消費蘋果 
06-22 10:40:06.778 25997-26046/com.android.mangodialog E/Consumer: 消費者消費蘋果結束 -----------------------
06-22 10:40:06.778 25997-26045/com.android.mangodialog E/Producer: 生產者生產蘋果 蘋果10號 結束----------------

當生產者生產到第10號蘋果的時候,佇列已經放了10個蘋果,這時候執行緒就被阻塞了,直到消費者消費了一個蘋果以後,生產者才生產成功了一個蘋果。

如果我們把邏輯改下,生產者每20s生產一個蘋果,消費者每過1s消費一個蘋果,那麼情況就是消費者就會被堵塞住,當20s的時候生產者生產了一個蘋果,消費者就開始成功消費一個蘋果,然後就繼續被阻塞了,知道下一個蘋果被成功生產。

原始碼解析

我們看看LinkedBlockingQueue的原始碼,前面說了它是一個單向連結串列阻塞佇列,看看原始碼中的節點類

節點類

/** 
 * Linked list node class. 
 */  
static class Node<E> {  
    E item;  
  
    /** 
     * One of: 
     * - the real successor Node 
     * - this Node, meaning the successor is head.next 
     * - null, meaning there is no successor (this is the last node) 
     */  
    Node<E> next;  
  
    Node(E x) { item = x; }  
} 

節點類只維護了後一個節點,沒有前一個節點(後面講到LinkedBlockingDeque時會講到兩個節點),這就是它的單向連結串列由來。

變數

再看看這個類裡面定義的變數

    /** 連結串列容量,如果沒有指定,設定為Integer.MAX_VALUE */
    private final int capacity;

    /** 連結串列當前元素數量 */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * Head of linked list.
     * Invariant: head.item == null
     * 佇列頭部節點,並且頭部節點中的元素為null
     */
    transient Node<E> head;

    /**
     * Tail of linked list.
     * Invariant: last.next == null
     * 佇列尾部節點,並且尾部節點後面的一個節點為null
     */
    private transient Node<E> last;

    /** Lock held by take, poll, etc
     * 用於take、poll等方法將元素出隊的鎖
     * */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes
     * 當佇列為空時,儲存出隊執行緒
     * */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc
     * 用於put、offer等方法將元素入隊的鎖
     * */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts
     *  當佇列滿的時候,儲存入隊的執行緒
     * */
    private final Condition notFull = putLock.newCondition();

看到這裡定義了兩把鎖,一把控制出隊執行緒,一把控制入隊執行緒,這說明了同一時間只能有一個執行緒出隊,一個執行緒入隊,其餘執行緒都會被阻塞;並且用了AtomicInteger 來表示佇列元素個數,這是原子操作的Integer,也就是每次都是來讀取這個真正的記憶體中的值,而不是去讀快取,也就能保證出隊執行緒和入隊執行緒在操作佇列時是安全的。

構造方法

再看接下來構造方法

/**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     * 空的構造方法,預設設定佇列長度為Integer.MAX_VALUE
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater than zero
     *
     * 傳入一個佇列長度值,並且例項化了一個頭部節點和尾部節點,且節點內的元素為null
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}, initially containing the elements of the
     * given collection,
     * added in traversal order of the collection's iterator.
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     *
     *  傳入一個集合
     */
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        // Never contended, but necessary for visibility
        //獲取佇列入隊鎖,雖然這時候沒人競爭鎖,但是有必要鎖住
        putLock.lock();
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                //將元素入隊
                enqueue(new Node<E>(e));
                //將元素計數
                ++n;
            }
            //設定佇列長度
            count.set(n);
        } finally {
            //釋放鎖
            putLock.unlock();
        }
    }

這裡有一個必走的構造方法就是第二個,做了兩件事,指定佇列長度,例項化一個頭部節點和一個尾部節點,不過節點內的元素為null。

put操作和take操作

看看put操作和take操作

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        //以e建立節點
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //獲取入隊鎖
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             * 如果佇列滿了,就將執行緒放入到Condition等待佇列中
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            //將節點放入到佇列中
            enqueue(node);
            //獲取當前佇列長度,然後將count加一
            c = count.getAndIncrement();
            //如果佇列沒有滿,就通知其它的入隊執行緒
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            //釋放入隊鎖
            putLock.unlock();
        }
        //如果佇列長度從0到非0,那就通知出隊執行緒來取資料
        if (c == 0)
            signalNotEmpty();
    }
public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        //獲取出隊鎖
        takeLock.lockInterruptibly();
        try {
            //如果佇列是空的,就將執行緒放入到Condition等待佇列中
            while (count.get() == 0) {
                notEmpty.await();
            }
            //取出一個節點元素
            x = dequeue();
            //獲取取出之前的佇列長度
            c = count.getAndDecrement();
            //如果佇列不為空,通知Condition等待佇列中的執行緒來取資料
            if (c > 1)
                notEmpty.signal();
        } finally {
            //釋放鎖
            takeLock.unlock();
        }
        //如果佇列長度從滿到非滿,通知入隊執行緒生產資料
        if (c == capacity)
            signalNotFull();
        return x;
    }
/**
     * Signals a waiting take. Called only from put/offer (which do not
     * otherwise ordinarily lock takeLock.)
     * 通知出隊執行緒,佇列已經不為空了,可以來獲取元素了
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        //獲取元素出隊的鎖
        takeLock.lock();
        try {
            //釋放出隊執行緒的第一個等待執行緒
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

    /**
     * Signals a waiting put. Called only from take/poll.
     * 通知入隊執行緒,佇列沒有滿,可以來新增元素了
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        //獲取元素入隊的鎖
        putLock.lock();
        try {
            //釋放入隊執行緒的第一個執行緒
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

/**
     * Links node at end of queue.
     *  建立一個節點,新增到連結串列尾部
     * @param node the node
     */
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        //封裝新節點,並賦給當前的最後一個節點的下一個節點,然後將這個節點設為最後一個節點
        last = last.next = node;
    }

    /**
     * Removes a node from head of queue.
     * 從佇列頭部移除一個節點
     * @return the node
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        //獲取頭節點 元素為null
        Node<E> h = head;
        //將頭節點的下一個節點賦值給first
        Node<E> first = h.next;
        //將當前將要出隊的節點置為null,為了使其做head節點做準備
        h.next = h; // help GC
        //將當前要出隊的節點作為頭節點
        head = first;
        //獲取出隊節點的元素值
        E x = first.item;
        //將出隊節點的元素值置為null
        first.item = null;
        return x;
    }

註釋描述的很清楚了,其實最主要的是要理解出隊dequeue和入隊enqueue兩個方法的邏輯

節點next

首先我們來看下節點類中的next的這段註釋

/**
  * One of:
  * - the real successor Node
  * - this Node, meaning the successor is head.next
  * - null, meaning there is no successor (this is the last node)
  */
 Node<E> next;

也就是說當前節點的下一個節點才是我們真正去操作的節點,如果這個節點的下一個節點為null,那這個節點就是最後一個節點;

這裡寫圖片描述

這可以看出head.item=null,last.next = null

還有一個意思其實是head這個節點從初始化的時候就能看出內部元素為null,如下圖

這裡寫圖片描述

當我們呼叫構造方法的時候,內部有這麼一句程式碼
last = head = new Node(null);
就是將last引用和head引用都指向了這個new的節點,元素為null

接下來就是我們呼叫put方法新增節點的時候了,從上面程式碼可以看出,最終會走到enqueue這個方法,就一句程式碼
last = last.next = node;
先將新封裝的節點引用賦值給了last.next;但是這時候last的引用還是和head一樣指向第一個節點(這時候的last.next和head.next是一樣的),所以這時候第二個等號將last.next的引用又賦值給了last
這裡寫圖片描述

然後呼叫take方法出隊,看看出隊邏輯

//獲取頭節點 元素為null
Node<E> h = head;
//將頭節點的下一個節點賦值給first
Node<E> first = h.next;
//將當前將要出隊的節點指到head節點的位置
h.next = h; // help GC
//將當前要出隊的節點作為頭節點
head = first;
//獲取出隊節點的元素值
E x = first.item;
//將出隊節點的元素值置為null
first.item = null;
return x;

執行完第一句和第二句程式碼後
這裡寫圖片描述

邏輯就是:將頭節點的引用給到了h,將新增加的節點的引用給到了first,接下來執行最後的程式碼

這裡寫圖片描述
第三步:
將第二個節點也就是要出隊的節點指向head節點,
第四步:
將當前要出對的節點引用賦值給head;這一步走完,存在堆記憶體中的第一個節點就沒有全域性變數的引用指向它
第五步:
把要出隊的節點的元素取出來賦值給x
第五步:
將第二個節點元素置為null,然後將元素返回

當這個方法執行完畢,最開始的第一個節點沒有全域性變數引用指向它,方法內的引用會從棧內回收,第一個節點的堆記憶體最終也會被回收,而第二個節點中的元素置為null,就這樣它變成了第一個節點,同樣的只有兩個全域性變數的引用head和last指向它。

這裡寫圖片描述

相關文章