一、介紹
1.概念
ConcurrentHashMap是HashMap的線程安全版本,相對 HashMap 和 Hashtable, ConcurrentHashMap 增加了 Segment 層,每個 Segment 原理上等同於一個 Hashtable, ConcurrentHashMap 爲 Segment 的數組。
向 ConcurrentHashMap 中插入數據或者讀取數據,首先都要講相應的 Key 映射到對應的 Segment,因此不用鎖定整個類, 只要對單個的 Segment 操作進行上鎖操作就可以了。理論上如果有 n 個 Segment,那麼最多可以同時支持 n 個線程的併發訪問,從而大大提高了併發訪問的效率。
注:hashmap的key和value可以爲null,但是concurrentHashMap的key和value不能爲null
2. ConcurrentHashMap 在 jdk1.7 的升級到 1.8 中的變化
-
改進一:取消 segments 字段,直接採用 transient volatile HashEntry<K,V>[] table 保存數據,採用 table 數組元素作爲鎖,鎖的粒度從多個 Node 級別又減小到一個 Node 級別,再度減小鎖競爭,減小程序同步的部分。
-
改進二:將原先 table 數組+單向鏈表的數據結構,變更爲 table 數組+單向鏈表+紅黑樹的結構。
二、final關鍵字
在講述ConcurrentHashMap類之前,先講一下final關鍵字
(一)基礎作用
- final修飾的字段不可修改,如果是引用,引用的地址不可改變。
- final修飾的方法不能重寫
- final修飾的類不可繼承
(二)final 的重排序規則
- 在構造函數內對一個 final 域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
- 初次讀一個包含 final 域的對象的引用,與隨後初次讀這個 final 域,這兩個操作之間不能重排序。
簡而言之:構造函數和對final域的讀寫操作不能重排序。
(三)final的使用場景
- 任何不希望域被改變的時候都可以使用 final
- 如果一個對象可以被多個線程訪問到,要麼被聲明爲 final,要麼需要提供額外的線程安全機制。其他的方式包括聲明爲 volatile 、使用 synchronized、顯示鎖等
三、實現
1.初始化操作(put操作)的線程安全
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
//value 和 next 指針使用了 volatile 來保證其可見性
...
}
有多個線程同時進行 put 操作(只有put操作的時候纔會初始化數組),在初始化數組時使用了樂觀鎖 CAS 操作來決定到底是哪個線程有資格進行初始化,其他線程均只能等待。
- volatile 變量(sizeCtl):它是一個標記位,用來告訴其他線程這個坑位有沒有人在,其線程間的可見性由 volatile 保證。
- CAS 操作:CAS 操作保證了設置 sizeCtl 標記位的原子性,保證了只有一個線程能設置成功
2.size()/統計當前存儲元素個數的線程安全
- 先利用 CAS 遞增 Count 值來感知是否存在線程競爭,若競爭不大直接 CAS 遞增 Count 值即可,性能與直接 Count++ 差別不大
- 如果有線程競爭,則CAS失敗,則初始化桶,利用桶計數,此時是分而治之的思想來計數,同時使用CAS來計數,最大化利用並行。如果桶計數失敗,則擴容桶
在設計中,使用了分而治之的思想,將每一個計數都分散到各個 countCell 對象裏面(下面稱之爲桶),使競爭最小化,又使用了 CAS 操作,就算有競爭,也可以對失敗了的線程進行其他的處理。樂觀鎖的實現方式與悲觀鎖不同之處就在於樂觀鎖可以對競爭失敗了的線程進行其他策略的處理,而悲觀鎖只能等待鎖釋放,所以這裏使用 CAS 操作對競爭失敗的線程做了其他處理,很巧妙的運用了 CAS 樂觀鎖。
【Java 面試那點事】
這裏致力於分享 Java 面試路上的各種知識,無論是技術還是經驗,你需要的這裏都有!
這裏可以讓你【快速瞭解 Java 相關知識】,並且【短時間在面試方面有跨越式提升】
面試路上,你不孤單!