1. 程式人生 > >java ConcurrentHashMap和CopyOnWriteArrayList解決併發問題

java ConcurrentHashMap和CopyOnWriteArrayList解決併發問題

ConcurrentHashMap

一、hashtable、hashmap、ConcurrentHashMap

1、執行緒不安全的HashMap

  因為多執行緒環境下,使用Hashmap進行put操作會引起死迴圈,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap。

2、效率低下的HashTable

  HashTable容器使用synchronized來保證執行緒安全,但線上程競爭激烈的情況下HashTable的效率非常低下。因為當一個執行緒訪問HashTable的同步方法時,其他執行緒訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如執行緒1使用put進行新增元素,執行緒2不但不能使用put方法新增元素,並且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。

3、鎖分段技術

  HashTable容器在競爭激烈的併發環境下表現出效率低下的原因,是因為所有訪問HashTable的執行緒都必須競爭同一把鎖,那假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。

二、ConcurrentHashMap的資料結構

  ConcurrentHashMap為了提高本身的併發能力,在內部採用了一個叫做Segment的結構,一個Segment其實就是一個類Hash Table的結構,同HashMap一樣,Segment包含一個HashEntry陣列,陣列中的每一個HashEntry既是一個鍵值對,也是一個連結串列的頭節點。可以說,ConcurrentHashMap是一個二級雜湊表。在一個總的雜湊表下面,有若干個子雜湊表。

 

  ConcurrentHashMap定位一個元素的過程需要進行兩次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的連結串列的頭部,因此,這一種結構的帶來的副作用是Hash的過程要比普通的HashMap要長,但是帶來的好處是寫操作的時候可以只對元素所在的Segment進行加鎖即可,不會影響到其他的Segment,這樣,在最理想的情況下,ConcurrentHashMap可以最高同時支援Segment數量大小的寫操作(剛好這些寫操作都非常平均地分佈在所有的Segment上),所以,通過這一種結構,ConcurrentHashMap的併發能力可以大大的提高。

三、Segment

  Segment繼承了ReentrantLock,所以它就是一種可重入鎖(ReentrantLock)。在ConcurrentHashMap,一個Segment就是一個子雜湊表,Segment裡維護了一個HashEntry陣列,併發環境下,對於不同Segment的資料進行操作是不用考慮鎖競爭的。

四、get、put、remove

Get方法:

1.為輸入的Key做Hash運算,得到hash值。

2.通過hash值,定位到對應的Segment物件

3.再次通過hash值,定位到Segment當中陣列的具體位置。

Put方法:

1.為輸入的Key做Hash運算,得到hash值。

2.通過hash值,定位到對應的Segment物件

3.獲取可重入鎖

4.再次通過hash值,定位到Segment當中陣列的具體位置。

5.插入或覆蓋HashEntry物件。

6.釋放鎖。

Remove方法:

  定位元素通put方法,不過這裡刪除元素的方法不是簡單地把待刪除元素的前面的一個元素的next指向後面一個就完事了,由於HashEntry中的next是final的,一經賦值以後就不可修改,在定位到待刪除元素的位置以後,程式就將待刪除元素前面的那一些元素全部複製一遍,然後再一個一個重新接到連結串列上去

 

五、size()方法

1.遍歷所有的Segment。

2.把Segment的元素數量累加起來。

3.把Segment的修改次數累加起來。

4.判斷所有Segment的總修改次數是否大於上一次的總修改次數。如果大於,說明統計過程中有修改,重新統計,嘗試次數+1;如果不是。說明沒有修改,統計結束。

5.如果嘗試次數超過閾值,則對每一個Segment加鎖,再重新統計。

6.再次判斷所有Segment的總修改次數是否大於上一次的總修改次數。由於已經加鎖,次數一定和上次相等。

7.釋放鎖,統計結束。

為了儘量不鎖住所有Segment,首先樂觀地假設Size過程中不會有修改。當嘗試一定次數,才無奈轉為悲觀鎖,鎖住所有Segment保證強一致性。

六、jdk1.7和1.8中的區別

  JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node陣列+連結串列+紅黑樹的資料結構來實現,併發控制使用Synchronized和CAS來操作,整個看起來就像是優化過且執行緒安全的HashMap,雖然在JDK1.8中還能看到Segment的資料結構,但是已經簡化了屬性,只是為了相容舊版本

  改進一:取消segments欄位,直接採用transient volatile HashEntry<K,V>[] table儲存資料,採用table陣列元素作為鎖,從而實現了對每一行資料進行加鎖,進一步減少併發衝突的概率。

  改進二:將原先table陣列+單向連結串列的資料結構,變更為table陣列+單向連結串列+紅黑樹的結構。對於hash表來說,最核心的能力在於將key hash之後能均勻的分佈在陣列中。如果hash之後雜湊的很均勻,那麼table陣列中的每個佇列長度主要為0或者1。但實際情況並非總是如此理想,雖然ConcurrentHashMap類預設的載入因子為0.75,但是在資料量過大或者運氣不佳的情況下,還是會存在一些佇列長度過長的情況,如果還是採用單向列表方式,那麼查詢某個節點的時間複雜度為O(n);因此,對於個數超過8(預設值)的列表,jdk1.8中採用了紅黑樹的結構,那麼查詢的時間複雜度可以降低到O(logN),可以改進效能。

CopyOnWriteArrayList

一、arraylist的問題

 多執行緒時,如果遍歷過程中另一個執行緒對list進行插入以後,遍歷報錯。

二、CopyOnWriteArrayList的實現原理

  CopyOnWrite容器即寫時複製的容器。通俗的理解是當我們往一個容器新增元素的時候,不直接往當前容器新增,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裡新增元素,新增完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因為當前容器不會新增任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。

三、CopyOnWrite的應用場景

  CopyOnWrite併發容器用於讀多寫少的併發場景。比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜尋網站,使用者在這個網站的搜尋框中,輸入關鍵字搜尋內容,但是某些關鍵字不允許被搜尋。這些不能被搜尋的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當用戶搜尋時,會檢查當前關鍵字在不在黑名單當中,如果在,則提示不能搜尋。

四、缺點

  1.記憶體佔用問題。因為CopyOnWrite的寫時複製機制,所以在進行寫操作的時候,記憶體裡會同時駐紮兩個物件的記憶體,舊的物件和新寫入的物件(注意:在複製的時候只是複製容器裡的引用,只是在寫的時候會建立新物件新增到新容器裡,而舊容器的物件還在使用,所以有兩份物件記憶體)。

  2.資料一致性問題。CopyOnWrite容器只能保證資料的最終一致性,不能保證資料的實時一致性。所以如果你希望寫入的的資料,馬上能讀到,請不要使用CopyOnWrite容器。

五、總結

1.執行緒安全,讀操作時無鎖的ArrayList

2.底層資料結構是一個Object[],初始容量為0,之後每增加一個元素,容量+1,陣列複製一遍

3.增刪改上鎖、讀不上鎖

4.遍歷過程由於遍歷的只是全域性陣列的一個副本,即使全域性陣列發生了增刪改變化,副本也不會變化,所以不會發生併發異常

5.讀多寫少且髒資料影響不大的併發情況下,選擇CopyOnWriteArrayList