J.U.C 中的阻塞佇列
![](https://i.iter01.com/images/cb21dfc854a47bd9e5fa0f85fd9710bbc5a24c6bc31a3f55d55490e058847fbf.png)
阻塞佇列的操作方法
在阻塞佇列中,提供了四種處理方式
1. 插入操作
add(e) :新增元素到佇列中,如果佇列滿了,繼續插入元素會報錯,IllegalStateException。
offer(e) : 新增元素到佇列,同時會返回元素是否插入成功的狀態,如果成功則返回 trueput(e) :當阻塞佇列滿了以後,生產者繼續通過 put新增元素,佇列會一直阻塞生產者執行緒,知道佇列可用
offer(e,time,unit) :當阻塞佇列滿了以後繼續新增元素,生產者執行緒會被阻塞指定時間,如果超時,則執行緒直接退出
2. 移除操作
remove():當佇列為空時,呼叫 remove 會返回 false,如果元素移除成功,則返回 true
poll(): 當佇列中存在元素,則從佇列中取出一個元素,如果佇列為空,則直接返回 null
take():基於阻塞的方式獲取佇列中的元素,如果佇列為空,則 take 方法會一直阻塞,直到佇列中有新的資料可以消費
poll(time,unit):帶超時機制的獲取資料,如果佇列為空,則會等待指定的時間再去獲取元素返回
ArrayBlockingQueue 原理分析
構造方法
ArrayBlockingQueue 提供了三個構造方法,分別如下。
capacity: 表示陣列的長度,也就是佇列的長度fair:表示是否為公平的阻塞佇列,預設情況下構造的是非公平的阻塞佇列。
其中第三個構造方法就不解釋了,它提供了接收一個幾個作為資料初始化的方法
public ArrayBlockingQueue(int capacity) { this(capacity, false); } public ArrayBlockingQueue(int capacity,boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = new Object[capacity]; lock = new ReentrantLock(fair); //重入鎖,出隊和入隊持有這一把鎖 notEmpty = lock.newCondition(); //初始化非空等待佇列 notFull = lock.newCondition(); //初始化非滿等待佇列 }
關於鎖的用途,大家在沒有看接下來的原始碼之前,可以先思考一下他的作用。items 構造以後,大概是一個這樣的陣列結構
![](https://i.iter01.com/images/a800dc0838f15e2172b86e0d28650323a60fea160039bfc1207896c5698e9d1f.png)
Add 方法
以 add 方法作為入口,在 add 方法中會呼叫父類的 add 方法,也就是 AbstractQueue.如果看原始碼看得比較多的話,
一般這種寫法都是呼叫父類的模版方法來解決通用性問題
public boolean add(E e) { return super.add(e); }
從父類的 add 方法可以看到,這裡做了一個佇列是否滿了的判斷,如果佇列滿了直接丟擲一個異常
public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); }
offer 方法
add 方法最終還是呼叫 offer 方法來新增資料,返回一個新增成功或者失敗的布林值反饋
這段程式碼做了幾個事情
1. 判斷新增的資料是否為空
2. 新增重入鎖
3. 判斷佇列長度,如果佇列長度等於陣列長度,表示滿了直接返回 false
4. 否則,直接呼叫 enqueue 將元素新增到佇列中
public boolean offer(E e) { checkNotNull(e); //對請求資料做判斷 final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length) return false; else { enqueue(e); return true; } } finally { lock.unlock(); } }
enqueue
這個是最核心的邏輯,方法內部通過 putIndex 索引直接將元素新增到陣列 items
private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; //通過 putIndex 對資料賦值 if (++putIndex == items.length) // 當putIndex 等於陣列長度時,將 putIndex 重置為 0 putIndex = 0; count++;//記錄佇列元素的個數 notEmpty.signal();//喚醒處於等待狀態下的執行緒,表示當前佇列中的元素不為空,如果存在消費者執行緒阻塞,就可以開始取出元素 }
這裡大家肯定會有一個疑問,putIndex 為什麼會在等於陣列長度的時候重新設定為 0。因為 ArrayBlockingQueue 是一個 FIFO 的佇列,佇列新增元素時,是從隊尾獲取 putIndex 來儲存元素,當 putIndex等於陣列長度時,下次就需要從陣列頭部開始新增了。
下面這個圖模擬了新增到不同長度的元素時,putIndex 的變化,當 putIndex 等於陣列長度時,不可能讓 putIndex 繼續累加,否則會超出陣列初始化的容量大小。同時大家還需要思考兩個問題
1. 當元素滿了以後是無法繼續新增的,因為會報錯
2. 其次,佇列中的元素肯定會有一個消費者執行緒通過 take或者其他方法來獲取資料,而獲取資料的同時元素也會從佇列中移除
![](https://i.iter01.com/images/7b0cb37db0c4fcc7b4f4f630c07ef5386683255b8d238bfd6c2c70491afbc65a.png)
put 方法
put 方法和 add 方法功能一樣,差異是 put 方法如果佇列滿了,會阻塞。這個在最開始的時候說過。接下來看一下它的實現邏輯
public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); //這個也是獲得鎖,但是和 lock 的區別是,這個方法優先允許在等待時由其他執行緒呼叫等待執行緒的 interrupt 方法來中斷等待直接返回。而 lock方法是嘗試獲得鎖成功後才響應中斷 try { while (count == items.length) notFull.await();//佇列滿了的情況下,當前執行緒將會被 notFull 條件物件掛起加到等待佇列中 enqueue(e); } finally { lock.unlock(); } }
take 方法
take 方法是一種阻塞獲取佇列中元素的方法它的實現原理很簡單,有就刪除沒有就阻塞,注意這個阻塞是可以中斷的,如果佇列沒有資料那麼就加入 notEmpty條件佇列等待(有資料就直接取走,方法結束),如果有新的put 執行緒新增了資料,那麼 put 操作將會喚醒 take 執行緒,執行 take 操作
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); //如果佇列為空的情況下,直接通過 await 方法阻塞 return dequeue(); } finally { lock.unlock(); } }
如果佇列中新增了元素,那麼這個時候,會在 enqueue 中呼叫 notempty.signal 喚醒 take 執行緒來獲得元素
![](https://i.iter01.com/images/78bc7bf491dd7a90c6ffbc468885e5df1bc6166734ba2eadb1e70c67c61d0306.png)
dequeue 方法
這個是出佇列的方法,主要是刪除佇列頭部的元素併發返回給客戶端takeIndex,是用來記錄拿資料的索引值
private E dequeue() { // assert lock.getHoldCount() == 1; // assert items[takeIndex] != null; final Object[] items = this.items; @SuppressWarnings("unchecked") E x = (E) items[takeIndex]; //預設獲取 0 位置的元素 items[takeIndex] = null;//將該位置的元素設定為空 if (++takeIndex == items.length)//這裡的作用也是一樣,如果拿到陣列的最大值,那麼重置為 0,繼續從頭部位置開始獲取資料 takeIndex = 0; count--;//記錄 元素個數遞減 if (itrs != null) itrs.elementDequeued();//同時更新迭代器中的元素資料 notFull.signal();//觸發 因為佇列滿了以後導致的被阻塞的執行緒 return x; }
itrs.elementDequeued();
ArrayBlockingQueue 中,實現了迭代器的功能,也就是可以通過迭代器來遍歷阻塞佇列中的元素
![](https://i.iter01.com/images/78a08833ea5e7a6070d5dd9cf2c4cf2eb29717ca99d21dca95ccd4c58050c601.png)
所以 itrs.elementDequeued() 是用來更新迭代器中的元素資料的takeIndex 的索引變化圖如下,同時隨著資料的移除,會喚醒處於 put 阻塞狀態下的執行緒來繼續新增資料
![](https://i.iter01.com/images/87d3dd7b4f896fe9c68cfc8ee648deaf0df5ae5f16831aa13497b40b5dc48238.png)
remove 方法
remove 方法是移除一個指定元素。看看它的實現程式碼
public boolean remove(Object o) { if (o == null) return false; final Object[] items = this.items; //獲取數組元素 final ReentrantLock lock = this.lock; lock.lock(); //獲得鎖 try { if (count > 0) { //如果佇列不為空 final int putIndex = this.putIndex; //獲取下一個要新增元素時的索引 int i = takeIndex;//獲取當前要被移除的元素的索引 do { if (o.equals(items[i])) {//從takeIndex 下標開始,找到要被刪除的元素 removeAt(i);//移除指定元素 return true;//返回執行結果 } //當前刪除索引執行加 1 後判斷是否與陣列長度相等 //若為 true,說明索引已到陣列盡頭,將 i 設定為 0 if (++i == items.length) i = 0; } while (i != putIndex);//繼續查詢,直到找到最後一個元素 } return false; } finally { lock.unlock(); } }
原子操作類
原子性這個概念,在多執行緒程式設計裡是一個老生常談的問題。
所謂的原子性表示一個或者多個操作,要麼全部執行完,要麼一個也不執行。不能出現成功一部分失敗一部分的情況。
在多執行緒中,如果多個執行緒同時更新一個共享變數,可能會得到一個意料之外的值。比如 i=1 。A 執行緒更新 i+1 、B 執行緒也更新 i+1。
通過兩個執行緒並行操作之後可能 i 的值不等於 3。而可能等於 2。因為 A 和 B 在更新變數 i 的時候拿到的 i 可能都是 1
這就是一個典型的原子性問題
前面幾節課我們講過,多執行緒裡面,要實現原子性,有幾種方法,其中一種就是加 Synchronized 同步鎖。
而從 JDK1.5 開始,在 J.U.C 包中提供了 Atomic 包,提供了對於常用資料結構的原子操作。它提供了簡單、高效、以及執行緒安全的更新一個變數的方式
J.U.C 中的原子操作類
由於變數型別的關係,在 J.U.C 中提供了 12 個原子操作的類。這 12 個類可以分為四大類
1. 原子更新基本型別
AtomicBoolean、AtomicInteger、AtomicLong
2. 原子更新陣列
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
3. 原子更新引用
AtomicReference 、 AtomicReferenceFieldUpdater 、AtomicMarkableReference(更新帶有標記位的引用型別)
4. 原子更新欄位
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference
AtomicInteger 原理分析
接下來,我們來剖析一下 AtomicInteger 的實現原理,仍然是基於我們剛剛在前面的案例中使用到的方法作為突破口
getAndIncrement
getAndIncrement 實際上是呼叫 unsafe 這個類裡面提供的方法,
Unsafe 類我們前面在分析 AQS 的時候講過,這個類相當於是一個後門,使得 Java 可以像 C 語言的指標一樣直接操作記憶體空間。當然也會帶來一些弊端,就是指標的問題。
實際上這個類在很多方面都有使用,除了 J.U.C 這個包以外,還有 Netty、kafka 等等這個類提供了很多功能,包括多執行緒同步(monitorEnter)、CAS 操 作 (compareAndSwap) 、執行緒的掛起和恢復(park/unpark)、記憶體屏障(loadFence/storeFence)
記憶體管理(記憶體分配、釋放記憶體、獲取記憶體地址等.)
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
valueOffset,也比較熟了。通過 unsafe.objectFieldOffset()獲取當前 Value 這個變數在記憶體中的偏移量,後續會基於這個偏移量從記憶體中得到value的值來和當前的值做比較,實現樂觀鎖
private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value") ); } catch (Exception ex) { throw new Error(ex); } }
getAndAddInt
通過 do/while 迴圈,基於 CAS 樂觀鎖來做原子遞增。實際上前面的 valueOffset 的作用就是從主記憶體中獲得當前value 的值和預期值做一個比較,如果相等,對 value 做遞增並結束迴圈
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
get 方法
get 方法只需要直接返回 value 的值就行,這裡的 value 是通過 Volatile 修飾的,用來保證可見性
public final int get() { return value; }
其他方法
AtomicInteger 的實現非常簡單,所以我們可以很快就分析完它的實現原理,當然除了剛剛分析的這兩個方法之外,
還有其他的一些比 如 它 提 供 了 compareAndSet , 允 許 客 戶 端 基 於AtomicInteger 來實現樂觀鎖的操作
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }