微信公衆號:放開我我還能學java
分享知識,共同進步!web
看你簡歷裏寫了 HashMap,那你說說它存在什麼缺點?數組
那你有用過線程安全的 Map 嗎?安全
有,回答在哪用過。微信
沒有,不過我瞭解過。多線程
那你說說它們的實現。併發
Hashtableapp
Hashtable 自己比較低效,由於它的實現基本就是將 put、get、size 等各類方法加上 synchronized 鎖。這就致使了全部併發操做都要競爭同一把鎖,一個線程在進行同步操做時,其餘線程只能等待,大大下降了併發操做的效率。編輯器
Collections#SynchronizedMapide
同步包裝器 SynchronizedMap 雖然沒使用方法級別的 synchronized 鎖,可是使用了同步代碼塊的形式,本質上仍是沒有改進。
ConcurrentHashMap
首先 ConcurrentHashMap 在 JDK1.7 和 JDK1.8 的實現方式是不一樣的。
在 JDK1.7 中,ConcurrentHashMap 是由 Segment
數組結構和 HashEntry
數組結構組成。Segment 繼承了 ReentrantLock,是一種可重入鎖。HashEntry 則用於存儲鍵值對數據。一個 ConcurrentHashMap 裏包含一個 Segment 數組,一個 Segment 裏包含一個 HashEntry 數組 ,每一個 HashEntry 是一個鏈表結構的元素,所以 JDK1.7 的 ConcurrentHashMap 是一種數組+鏈表結構。當對 HashEntry 數組的數據進行修改時,必須首先得到與它對應的 Segment 鎖,這樣只要保證每一個 Segment 是線程安全的,也就實現了全局的線程安全(分段鎖)。
在 JDK1.8 中,ConcurrentHashMap 選擇了與 HashMap 相同的數組+鏈表+紅黑樹結構,在鎖的實現上,採用 CAS 操做和 synchronized 鎖實現更加低粒度的鎖,將鎖的級別控制在了更細粒度的 table 元素級別,也就是說只須要鎖住這個鏈表的首節點,並不會影響其餘的 table 元素的讀寫,大大提升了併發度。
那爲何 JDK1.8 要使用 synchronized 鎖而不是其餘鎖呢?
我認爲有如下兩個方面:
你提到了 synchronized 的鎖狀態升級,能具體說下每種鎖狀態在什麼狀況下升級嗎?
無鎖
無鎖沒有對資源進行鎖定,全部的線程都能訪問並修改同一個資源,但同時只有一個線程能修改爲功。
偏向鎖
當一段同步代碼一直被同一個線程所訪問,無鎖就會升級爲偏向鎖,之後該線程訪問該同步代碼時會自動獲取鎖,下降了獲取鎖的代價。
輕量級鎖
當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,從而提升性能。
重量級鎖
若當前只有一個等待線程,則該線程經過自旋進行等待。可是當自旋超過必定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級爲重量級鎖。
爲何 ConcurrentHashMap 的 key 和 value 不能爲 null?
這是由於當經過 get(k)
獲取對應的 value 時,若是獲取到的是 null 時,沒法判斷,它是 put(k,v)
的時候 value 爲 null,仍是這個 key 歷來沒有添加。
假如線程 1 調用 map.contains(key)
返回 true,當再次調用 map.get(key)
時,map 可能已經不一樣了。由於可能線程 2 在線程 1 調用 map.contains(key)
時,刪除了 key,這樣就會致使線程 1 獲得的結果不明確,產生多線程安全問題,所以,ConcurrentHashMap 的 key 和 value 不能爲 null。
其實這是一種安全失敗機制(fail-safe),這種機制會使你這次讀到的數據不必定是最新的數據。
那你談談快速失敗(fail-fast)和安全失敗(fail-safe)的區別。
快速失敗和安全失敗都是 Java 集合中的一種機制。
若是採用快速失敗機制,那麼在使用迭代器對集合對象進行遍歷的時候,若是 A 線程正在對集合進行遍歷,此時 B 線程對集合進行增長、刪除、修改,或者 A 線程在遍歷過程當中對集合進行增長、刪除、修改,都會致使 A 線程拋出 ConcurrentModificationException 異常。
爲何在用迭代器遍歷時,修改集合就會拋異常時?
緣由是迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個 modCount 變量。集合在被遍歷期間若是內容發生變化,就會改變 modCount 的值。
每當迭代器使用 hashNext()/next() 遍歷下一個元素以前,都會檢測 modCount 變量是否爲 expectedModCount 值,是的話就返回遍歷;不然拋出異常,終止遍歷。
java.util 包下的集合類都是快速失敗的。
若是採用安全失敗機制,那麼在遍歷時不是直接在集合內容上訪問,而是先複製原有集合內容,在拷貝的集合上進行遍歷。
因爲迭代時是對原集合的拷貝進行遍歷,因此在遍歷過程當中對原集合所做的修改並不能被迭代器檢測到,故不會拋 ConcurrentModificationException 異常。
java.util.concurrent 包下的併發容器都是安全失敗的。
ConcurrentHashMap 的 get 方法爲何不用加鎖,會不會出現數據讀寫不一致狀況呢?
不會出現讀寫不一致的狀況。
get 方法邏輯比較簡單,只須要將 key 經過 hash 以後定位到具體的 Segment ,再經過一次 hash 定位到具體的元素上。
因爲變量 value
是由 volatile
修飾的,根據 JMM 中的 happen before
規則保證了對於 volatile 修飾的變量始終是寫操做先於讀操做的,而且 volatile 的內存可見性保證修改完的數據能夠立刻更新到主存中,因此能保證在併發狀況下,讀出來的數據是最新的數據。
說下 ConcurrentHashMap 的 put 方法執行邏輯。
JDK1.7:
先嚐試自旋獲取鎖,若是自旋重試的次數超過 64 次,則改成阻塞獲取鎖。獲取到鎖後:
JDK1.8:
先定位到 Node,拿到首節點 first,判斷是否爲:
first.hash = MOVED = -1
,說明其餘線程在擴容,參與一塊兒擴容。
first.hash != -1
,synchronized 鎖住 first 節點,判斷是鏈表仍是紅黑樹,遍歷插入。
說下 ConcurrentHashMap 的 size 方法如何計算最終大小。
JDK1.7:
雖然 count
變量是被 volatile
修飾的,可是並非簡單的把全部 Segment 的 count 值相加。由於有可能在累加過程當中 count 值發生了改變,那麼此時結果就不正確了。可是也不能直接鎖住,這樣效率過低。所以在 JDK1.7 中的作法是先嚐試 2 次經過不鎖住 Segment 的方式來統計各個 Segment 大小,若是統計的過程當中,容器的 count 發生了變化,則再採用加鎖的方式來統計全部Segment 的大小。
那麼 ConcurrentHashMap 是如何判斷在統計的時候容器是否發生了變化呢?使用modCount
變量,在put
、 remove
方法裏操做元素前都會將變量 modCount 進行加 1,那麼在統計大小先後比較 modCount 是否發生變化,從而得知容器的大小是否發生變化。
JDK1.8:
因爲沒有 segment 的概念,因此只須要用一個 baseCount
變量來記錄 ConcurrentHashMap 當前節點的個數。
counterCells
裏,後續數組 counterCells 的值會加到 baseCount 中。
如何提升 ConcurrentHashMap 的插入效率?
主要從如下兩個方面入手:
ConcurrentHashMap 是否存在線程不安全的狀況?若是存在的話,在什麼狀況下會出現?如何解決?
我想起來了,存在這種狀況,請看以下代碼
public class ConcurrentHashMapNotSafeDemo implements Runnable {
private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>(); public static void main(String[] args) throws InterruptedException { scores.put("John", 0); Thread t1 = new Thread(new ConcurrentHashMapNotSafeDemo()); Thread t2 = new Thread(new ConcurrentHashMapNotSafeDemo()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(scores); } @Override public void run() { for (int i = 0; i < 1000; i++) { Integer score = scores.get("John"); Integer newScore = score + 1; scores.put("John", newScore); } } } 複製代碼
結果輸出 {John=1323}
,很明顯發生了錯誤。
緣由就在於這三行在一塊兒不是線程安全的,雖然 get 方法和 put 方法是線程安全的,可是中間的又對獲取的值修改了,所以致使線程不安全。
解決方法是使用 replace 方法
能夠看到,replace 方法傳入三個值,分別是當前 key、舊值、新值
最終修改以下:
獲取更多最新文章,關注公衆號【放開我我還能學】