從原始碼解析-Android資料結構之單向阻塞佇列LinkedBlockingQueue的使用
前言
在一篇分析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指向它。
相關文章
- Java併發包原始碼學習系列:阻塞佇列實現之LinkedBlockingQueue原始碼解析Java原始碼佇列BloC
- 阻塞佇列 SynchronousQueue 原始碼解析佇列原始碼
- 阻塞佇列 DelayQueue 原始碼解析佇列原始碼
- 阻塞佇列--LinkedBlockingQueue佇列BloC
- 阻塞佇列 LinkedTransferQueue 原始碼解析佇列原始碼
- 阻塞佇列 PriorityBlockingQueue 原始碼解析佇列BloC原始碼
- 迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析JavaBloC佇列
- 資料結構之「佇列」資料結構佇列
- Python內建資料結構之雙向佇列Python資料結構佇列
- JavaScript資料結構之-佇列JavaScript資料結構佇列
- 資料結構之佇列(Queue)資料結構佇列
- JavaScript資料結構之佇列JavaScript資料結構佇列
- 併發佇列ConcurrentLinkedQueue和阻塞佇列LinkedBlockingQueue用法佇列BloC
- 看得見的資料結構Android版之佇列篇資料結構Android佇列
- Java併發包原始碼學習系列:阻塞佇列實現之SynchronousQueue原始碼解析Java原始碼佇列
- Java併發包原始碼學習系列:阻塞佇列實現之DelayQueue原始碼解析Java原始碼佇列
- [原始碼解析] 訊息佇列 Kombu 之 基本架構原始碼佇列架構
- 重學資料結構之佇列資料結構佇列
- 資料結構之「雙端佇列」資料結構佇列
- 資料結構學習之佇列資料結構佇列
- 資料結構之棧和佇列資料結構佇列
- Java併發包原始碼學習系列:阻塞佇列實現之LinkedTransferQueue原始碼解析Java原始碼佇列
- Java併發包原始碼學習系列:阻塞佇列實現之ArrayBlockingQueue原始碼解析Java原始碼佇列BloC
- Java併發包原始碼學習系列:阻塞佇列實現之PriorityBlockingQueue原始碼解析Java原始碼佇列BloC
- JavaScript資料結構之陣列棧佇列JavaScript資料結構陣列佇列
- 阻塞佇列 BlockingQueue(二)--ArrayBlockingQueue與LinkedBlockingQueue佇列BloC
- 資料結構-佇列資料結構佇列
- 【資料結構-----佇列】資料結構佇列
- 資料結構 - 佇列資料結構佇列
- 從簡單的線性資料結構開始:棧與佇列資料結構佇列
- Java併發包原始碼學習系列:阻塞佇列實現之LinkedBlockingDeque原始碼解析Java原始碼佇列BloC
- 【資料結構】佇列(順序佇列、鏈佇列)的JAVA程式碼實現資料結構佇列Java
- 資料結構之php實現佇列資料結構PHP佇列
- 資料結構二之棧和佇列資料結構佇列
- Python技術分享:內建資料結構之雙向佇列Python資料結構佇列
- Python教程:Python內建資料結構之雙向佇列!Python資料結構佇列
- Laravel 佇列原始碼解析Laravel佇列原始碼
- 佇列_單向連結串列佇列