目錄
- 阻塞隊列的定義和使用場景
- 阻塞的隊列的實現原理
- 簡單學習無鎖併發容器之ConcurrentLinkedQueue和CAS
- 資料
- 收穫
一、阻塞隊列的定義和使用場景
阻塞隊列(BlockingQueue)在隊列Queue的基礎上增加了兩個場景的阻塞
- 當隊列滿時,再向隊列添加數據會阻塞,直到隊列不滿時
- 當隊列爲空時,再向隊列獲取數據會阻塞,直到隊列變爲非空
阻塞隊列常用於生產者消費者的場景
下面我們先來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
接口有七個實現類,分別如下:
- ArrayBlockingQueue : 由數組結構組成的有界阻塞隊列,在添加和獲取時內部使用一個ReentrantLock可重入同步鎖
- 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>());
}
- SynchronousQueue:不存儲元素的阻塞隊列。每個插入操作必須等待另個一個線程調用的移除操作,否則一致處於阻塞狀態。吞吐量一般高於LinkedBlockingQueue。Executors#newCachedThreadPool()使用了這個阻塞隊列
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- PriorityBlockingQueue:支持優先級排序的無界阻塞隊列
- DelayQueue:使用優先級隊列實現的支持延遲獲取元素的無界阻塞隊列
- TransferQueue:鏈表結構組成的無界阻塞隊列
- 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”,備註 視頻編碼讀書寫作
,一起學習成長。
四、資料
- 圖書:《Java併發編程的藝術》
- 深入剖析java併發之阻塞隊列LinkedBlockingQueue與ArrayBlockingQueue
- Java併發編程-無鎖CAS與Unsafe類及其併發包Atomic
五、收穫
通過本篇的學習實踐
- 分析了java併發阻塞隊列的應用和實現
- 簡單分析學習了CAS和無鎖併發容器ConcurrentLinkedQueue
感謝你的閱讀,Java併發編程到這裏就暫告一段落,接下來一段時間會進入編碼的學習時間。
主要是針對《視頻編碼全角度詳解》這本書的閱讀和實踐。以21天爲一個週期(不一定要讀完,但是每天至少讀一頁,且至少輸出50字),有興趣的朋友可以一起來學習交流,加我微信“yabin_yangO2O”,備註 視頻編碼讀書寫作
下一篇我們開始視頻編碼知識的學習實踐,歡迎關注公衆號“音視頻開發之旅”,一起學習成長。
歡迎交流