前言
當我們身在分佈式開發中時經常會碰到突然大量的消息造訪,而我們的消費者無法及時處理,最終導致消息丟失,甚至服務崩潰。這個時候我們就需要暫時將這些不速之客請到“休息室”去坐一下。
阻塞隊列BlockingQueue就是我們經常使用的“休息室”。阻塞隊列可以有效的阻止大量的消息衝擊我們的服務,設置隊列大小可以將無法處理的消息阻止在外。本文將學習jdk1.7中加入的阻塞隊列--LinkedTransferQueue
一、LinkedTransferQueue簡介
1.1 命名
線性傳輸隊列 網上查不到它的合適的翻譯,我就給它取了一個名字--線性傳輸隊列。線性傳輸隊列的類繼承結構如圖所示
1.2 原理簡介
從圖中我們可以看到它底層是Collection和Queue,因此它是具有集合特性的,同時還具有Queue的基本功能。名字中還帶了Linked,說明他是鏈表形式的,然後它還引入了一個新的TransferQueue接口特性,有如下接口,接口的功能在下面講述
(1)boolean tryTransfer(E e);
(2)void transfer(E e) throws InterruptedException;
(3)boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException;
(4)boolean hasWaitingConsumer();
(5)int getWaitingConsumerCount();
1.3 LinkedTransferQueue核心方法
以我目前的能力只能分析到這裏了,光分析這個就有點勉強了,awaitMatch就不分析了
/**
* tryTransfer相關的方法底層實現都是通過xfer實現的
* 根據它的表現,這是一個拉和吃一體的方法,非常全能
*
* @param e 需要插入的信息
* @param haveData true表示數據插入,false表示數據請求
* @param how 操作類型,有四種類型:NOW(即時), ASYNC(異步), SYNC(同步), or TIMED(超時模式)
* @param nanos 超時時間,單位納秒
*/
private E xfer(E e, boolean haveData, int how, long nanos) {
// 如果選擇了有數據要插入,但是數據又是空的,就直接拋出異常
if (haveData && (e == null))
throw new NullPointerException();
// 非常複雜的一個邏輯,又沒有註釋,所以這裏的註釋都是我以爲,如果有問題請不吝賜噴
// 最外圈的for循環是沒有限制條件的,通過循環裏面的continue restart跳轉
restart: for (Node s = null, t = null, h = null; ; ) {
// 這個for節點的條件有點小複雜,但是沒關係,一步步來
// 這裏利用了java從左往右的特性
// t != (t = tail)這句話是,先拿到t的值,此時t=null,然後再將t賦值爲tail
// 最後做比較,null != tail,當然條件成立了,此時t已經是tail了,初始化時的tail.isData是true
// 最後結論當haveData時(存值) p=tail,當不是haveData時(取值)p=head,符合隊列特性
for (Node p = (t != (t = tail) && t.isData == haveData) ? t
: (h = head); ; ) {
final Node q;
final Object item;
// 如果haveData爲true(存數據)
// p不是數據節點且item是null,條件成立,反之不成立
// 如果haveData爲false(取數據)
// p是數據節點且item不是null,條件成立,反之不成立
// 從上述說明看出:
// 存數據時,p=tail不是數據節點進入;
// 取數據時,p=head是數據節點進入
// 但isData和item是一致的,所以取數據才進入這個條件,除非數據被取走了
// 下個請求進來就直接從新的head取數據了
if (p.isData != haveData
&& haveData == ((item = p.item) == null)) {
if (h == null) h = head;
// 嘗試匹配,匹配成功更新p節點
if (p.tryMatch(item, e)) {
// 取數據時,一般情況下(h=head)!=(p=head)是false,
// 可能別的線程在這個時候有操作,h和p之間已經有被取走的節點了
// 可能Node還在,但是裏面的item已經是空的了,所以要將p矯正成新的頭節點
if (h != p) skipDeadNodesNearHead(h, p);
return (E) item;
}
}
// 存數據時,p是尾節點,尾節點後沒有數據了,說明這就是個尾節點,能進入條件
// 取數據時,p是頭節點,頭結點後面如果是空的,也能進入條件
if ((q = p.next) == null) {
// 如果操作方式是NOW,那麼直接返回輸入的數據
if (how == NOW) return e;
// 給s創建一個空節點,e是空的就創建請求節點,否則創建數據節點
if (s == null) s = new Node(e);
// 更新next節點成功的話,到下個循環
if (!p.casNext(null, s)) continue;
// p不是尾節點,把s更新成最新的尾節點
if (p != t) casTail(t, s);
// 這個時候ASYNC就也需要返回了
if (how == ASYNC) return e;
// 旋轉等待數據
return awaitMatch(s, p, e, (how == TIMED), nanos);
}
// 退到最外面的for循環重新開始
if (p == (p = q)) continue restart;
}
}
}
判斷是否有已經等待的消費者,通過是否有空節點來判斷
public boolean hasWaitingConsumer() {
restartFromHead: for (;;) {
for (Node p = head; p != null;) {
Object item = p.item;
if (p.isData) {
// 如果隊列裏面還是有數據的
// 直接break
if (item != null)
break;
}
// 如果數據已經被人取走了,只剩下空節點了,
// 這個時候當前線程就是等待者,然後就返回true
else if (item == null)
return true;
if (p == (p = p.next))
continue restartFromHead;
}
return false;
}
}
通過當前空節點數目來判斷等待的消費者數目
/**
* 當前等待的消費者數目
* @return
*/
public int getWaitingConsumerCount() {
return countOfMode(false);
}
private int countOfMode(boolean data) {
restartFromHead: for (;;) {
int count = 0;
for (Node p = head; p != null;) {
// p節點是否被匹配了,當數據節點裏面的item是空的,匹配成功
if (!p.isMatched()) {
// 因爲p.isData是true,所以是數據請求時,
if (p.isData != data)
return 0;
// 等待累加,其實就是在循環內查看被其他線程取空的數據節點有多少個
if (++count == Integer.MAX_VALUE)
break; // @see Collection.size()
}
if (p == (p = p.next))
continue restartFromHead;
}
return count;
}
}
二、重要方法功能
通過上面的分析,我大致瞭解了LinkedTransferQueue類中幾個方法的功能
2.1 put 方法
顧名思義,這是一個存數據的方法
public void put(E e) {
xfer(e, true, ASYNC, 0);
}
異步存放插入數據,在tail後面插入新的節點,因爲整個數據結構是鏈表,所以是無界的,所以不會阻塞
2.2 offer 方法
offer有兩種方式,一種帶超時的,一直不帶超時的(表面上的)
public boolean offer(E e, long timeout, TimeUnit unit) {
xfer(e, true, ASYNC, 0);
return true;
}
public boolean offer(E e) {
xfer(e, true, ASYNC, 0);
return true;
}
從源碼中很容易判斷出,這是個障眼法,因爲本身就是不會阻塞的,所以超時時間就是個擺設,設置了沒用
2.3 add 方法
和上面的offer效果是一模一樣的
public boolean add(E e) {
xfer(e, true, ASYNC, 0);
return true;
}
2.4 tryTransfer 方法
有兩種,一種直接返回,一種超時返回
從上面的源碼看NOW操作方式都沒有創建新的節點,也就是不會把數據放到隊列中,直接給等待中的消費者,如果沒有等待中的,直接返回false,並且不會入隊
public boolean tryTransfer(E e) {
return xfer(e, true, NOW, 0) == null;
}
public boolean tryTransfer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null)
return true;
if (!Thread.interrupted())
return false;
throw new InterruptedException();
}
2.5 transfer方法
同步插入數據,如果沒有取數據的消費者,一直等待。中間不支持中斷線程,否則拋出異常
public void transfer(E e) throws InterruptedException {
if (xfer(e, true, SYNC, 0) != null) {
Thread.interrupted(); // failure possible only due to interrupt
throw new InterruptedException();
}
}
2.6 take 方法
操作方式是SYNC,一直等待,直到取到數據
public E take() throws InterruptedException {
E e = xfer(null, false, SYNC, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
2.7 poll 方法
有兩種方式,一種直接返回,一種帶超時時間的
直接返回的話可能就是空的數據,超時會阻塞線程,直到獲取到數據或者超時
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E e = xfer(null, false, TIMED, unit.toNanos(timeout));
if (e != null || !Thread.interrupted())
return e;
throw new InterruptedException();
}
public E poll() {
return xfer(null, false, NOW, 0);
}
2.8 getWaitingConsumerCount和hasWaitingConsumer
這兩個方法在上面已經做過源碼分析了,一個是獲取當前等待的線程數,一個是判斷當前有沒有在等待的
結語
xfer()這個方法裏面還有很多邏輯沒有弄懂,等下次完全讀懂了後再更新一下