併發基礎 11 併發 容器

2021-08-08 04:32:19 字數 3165 閱讀 3713

要實現乙個執行緒安全的佇列有兩個方式:一種是使用阻塞演算法,另一種是使用非阻塞演算法。

阻塞演算法:

使用阻塞演算法的佇列可以用乙個鎖(入隊和出隊同一把鎖)或兩把鎖(入隊和出隊用不同的鎖)來實現。

非阻塞的實現方式則可以使用迴圈cas的方式來實現。

concurrentlinkedqueue非阻塞執行緒安全佇列

concurrentlinkedqueue是乙個基於鏈結節點的無界執行緒安全的佇列

它採用fifo(先進先出)的規則對節點進行排序;

當我們新增乙個元素的時候,它會新增到佇列的尾部;

當我們獲取乙個元素時,它會返回佇列頭部的元素。

佇列中不允許null元素。

判斷佇列成員是否為空,不要使用size()方法,使用isempty()方法,因為size()方法會遍歷整個佇列。

concurrentlinkedqueue由head節點和tail節點組成,每個節點(node)由節點元素(item)和指向下乙個節點的引用(next)組成;

節點與節點之間就是通過這個next關聯起來,從而形成乙個鍊錶結構的佇列。

我們看下原始碼中的引數:

private transient volatile nodehead;

private transient volatile nodetail;

public concurrentlinkedqueue()

要是你看過hahsmap的原始碼,會發現,這個和entry鍊錶差不多..

預設情況下head節點儲存的元素為空,tail節點等於head節點

我們看下node的原始碼:

private static class node

boolean casitem(e cmp, e val)

void lazysetnext(nodeval)

boolean casnext(nodecmp, nodeval)

.....

}

concurrentlinkedqueue入隊操作

入佇列就是將新新增的node節點,新增到佇列的尾部。

在沒新增元素之前,head和tail都是node自己(head節點)。

a. 新增元素1:佇列更新head節點的next節點為"元素1節點",又因為tail節點預設情況下等於head節點,所以他們的next節點都指向"元素1節點"

b. 新增元素2:佇列首先設定"元素節點1"的next節點為"元素2節點",然後更新tail節點指向"元素2節點"

c. 新增元素3:設定tail節點為元素3節點

d. 新增元素4:設定元素3的next節點為元素4節點,然後將tail節點指向元素4節點。

通過觀察入隊過程以及head節點和tail節點的變化,發現入隊主要做兩件事:

第二:更新tail節點,如果tail節點的next節點不為空,則將入隊節點設定成tail節點,如果tail節點的next節點為空,則將入隊節點設定成tail的next節點,所以tail節點不總是尾節點。(理解這點很重要...)

(讓tail節點永遠作為佇列的尾節點,這樣實現的**量非常少,而且邏輯清晰、易懂,但是有個缺點,每次都要使用迴圈cas更新tail節點。

如果能減少cas更新tail節點的次數,就能提高入隊的效率;so,並不是每次節點入隊後都將tail節點更新成尾節點;而且隨著佇列長度越來越大,

每次入隊時定位尾節點的時間就越長,太消耗效能..)

我們來看下原始碼:

public boolean offer(e e) 

} // 如果p和它的下乙個節點相等。

// 則說明p節點和q節點都為空,表示佇列剛剛初始化,所以返回head節點

else if (p == q)

p = (t != (t = tail)) ? t : head;

else

// p有next節點,表示p的next節點是尾節點,則需要重新更新p後,將它指向next節點

p = (p != t && t != (t = tail)) ? t : q;

}}

(此原始碼來自jdk1.7,與原書有差別)

從原始碼角度來看,整個入隊過程主要做兩件事:

1. 定位出尾節點;

tail節點並不總是尾節點,所以每次入隊都必須通過tail節點來找到尾節點。

尾節點可能是tail節點也可能是tail節點的next節點

2. 使用cas演算法將入隊節點設定成尾節點的next節點,如不成功則重試。

concurrentlinkedqueue出佇列

出佇列就是從佇列中返回乙個節點元素,並清空該節點對元素的引用。

觀察上圖可以發現,並不是每次出佇列時都更新head節點,當head節點有元素時,直接彈出head節點中的元素,而不會更新head節點。

只有當head節點中沒有時,出隊操作才會更新head節點。

主要是,儘量減少cas更新head節點的消耗,這種做法可以提高出隊的效率。

concurrentlinkedqueue使用demo

public class concurrentlinkedqueuetest 

latch.await();

system.out.println("cost time : " + (system.currenttimemillis() - starttime) + " ms");

service.shutdown();

} /**

* 生產者

*/public static void offer()

} /**

* 消費

* * @author cyx

* @time 2023年7月31日上午9:34:02

*/static class pool implements runnable

latch.countdown();

} }}

併發程式設計9 併發容器

解決併發情況下的容器執行緒安全問題 譬如 vector,hashtable,都是給予同步鎖實現的 concurrent包下的類,大部分都是使用系統底層的實現,類似於native public class test09 latch.countdown for thread t arr try catc...

併發容器3

上篇部落格針對快取用個futuretask 來進行處理來解決兩個執行緒可能計算出來同樣的值的問題。在上篇部落格中的實現幾乎是完美的,它能夠表現出非常好的併發性,如果結果計算出來則立即返回,如果其他執行緒在計算該結果,那麼新的執行緒將一直等待這個結果被計算出來。這樣其實也沒有徹徹底底的解決兩個執行緒計...

Java併發容器

同步容器將所有容器狀態的訪問都序列化,以實現執行緒安全性。這種方式的代價會嚴重降低併發性,多個執行緒競爭容器的鎖時,吞吐量將嚴重降低。併發容器是針對多個執行緒併發訪問設計的。通過併發容器來代替同步容器,可以極大地提高伸縮性並降低風險。與hashmap一樣,concurrenhashmap也是乙個基於...