注:本博客資源來自於享學課堂,自己消化之後有修改
目錄
使用
除了map有的線程安全的put和get方法外,ConcurrentHashMap還有在併發下的public V putIfAbsent(K key, V value),如果key已經存在,直接返回value的值,不會進行替換。如果key不存在,就添加key和value,返回null,並且它是線程安全的。
在1.7下ConcurrentHashMap實現分析
在1.7下的實現原理圖
備註:建議有想畫這種圖的同學,可以使用processon,非常好用
ConcurrentHashMap是有Segment數組結構和HashEntry數組組成,hashEntry每個元素是鏈表;Segment繼承ReentrantLock,是一種可重入鎖,ConcurrentHashMap上鎖就是在Segment上。
HashEntry是一個數組,每個數組元素存放的是一個鏈表,每次對鏈表元素進行修改的時候,都必須獲得數組對應的Segment鎖
構造方法和初始化
public ConcurrentHashMap17(int initialCapacity,float loadFactor, int concurrencyLevel)
參數說明
參數 | 描述 | 默認值 |
initialCapacity | ConcurrentHashMap初始容量,默認是 | DEFAULT_INITIAL_CAPACITY=16 |
loadFactor | 負載因子閾值,用於控制大小調整。initailCapacity*loadFactor=HashMap的容量,負載越大,鏈表的數據越多,查找難度就會變大,負載越小,鏈表數據越少。 | DEFAULT_LOAD_FACTOR = 0.75f |
concurrencyLevel |
估計併發級別,即可能會有多少個線程共同修噶愛這個map,以此來確定Segment數組大小,默認是16,必須是2的備註,如果設置17,正是就是32 |
DEFAULT_CONCURRENCY_LEVEL = 16 |
get操作
get操作先經過一次再散列的到A,然後通過|A高位獲取到Senment[]的位置,然後通過A全部散列定位到在table[]中的位置。整個過程沒有加鎖,而是通過volatile保證get可以拿到最新值。
transient volatile HashEntry<K,V>[] table;
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
//準備定位hash的值
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
//拿到segment下的table數組
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
//遍歷table下的HashEntry鏈表
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
put操作
ConcurrentHashMap初始化的時候,會初始化Segment[0],其他的Segment在插入第一個值的時候纔會初始化
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
//定位所需要的hash值
int hash = hash(key);
//定位到元素在Segment[]數組中的位置
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
//初始化Segment[j],因爲整個map初始化的時候,只初始化了segment[0]
s = ensureSegment(j);
//把元素放到對應的Segment元素中
return s.put(key, hash, value, false);
}
s = ensureSegment(j);
多個線程進入同一個Segment[k],只要有一個成功就行了,使用CAS保證併發
s.put(key, hash, value, false);
Segment.put方法會艙室獲得鎖,如果沒有獲得所,調用scanAndLockForPut方法自旋等待獲得鎖
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
scanAndLockForPut通過while自旋獲取鎖
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
。。。
while (!tryLock()) {
。。。
}
return node;
}
獲取鎖之後,如果摸個HashEntry節點有相同的key,就更新HashEntry的value值;否則新建已給HashEntry節點,採用頭插法把他設置爲鏈表的新head節點,並將原節點設置爲head的下一個節點
新建過程中如果節點數超過threshold,就會調用rehash()對Segment中的數組進行擴容
擴容rehash操作:對table擴容
對table擴容成兩倍的時候,有些值在數組中的下標未變,有些值會變化爲i+capacity,舉例如下
原table長度是capacity=4,元素在table中位置
hash值 | 15 | 23 | 34 | 56 | 77 |
在table中下標 | 3=15%4 | 3=23%4 | 2=34%4 | 0=56%4 | 1=77%4 |
擴展一倍之後,通過這個方法,可以快速定位和減少元素重拍的次數
hash值 | 56 | 34 | 77 | 15,23 | ||||
下標 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
計算 | i | i | i+capacity=1+5=5 | i+capacity=3+4=7 |
remove操作
和put方法類似,都是在操作之前要拿到鎖,以保證操作的線程安全性
ConcurrentHashMap的弱一致性
遍歷鏈表中是否有形同key以獲得value。但是由於遍歷過程中,其他線程可能對鏈表結果做了調整,因此get和containsKey方法返回的可能是已經過時的數據,這一點是ConcurrentHashMap在弱一致性上的體現。如果要求強一致性,那麼必須使用Conllections.synchronizedMap()方法
在多線程下避免使用size、containsValue方法
在循環的方法判斷兩次,每個Segment中所有元素個數的和兩次相等才返回值,否則循環次數超過預定義值,就會對Segment進行加鎖,影響性能
在1.8下ConcurrentHashMap實現分析
1.8對ConcurrentHashMap的改進
1、取消Segment字段,直接採用transient volatile HashEntry<K,V>[] table保存數據,直接採用table數組元素作爲鎖,從而實現了縮小鎖的粒度,提高了效率
2、原來是table數組+單項鍊表;現在是table數組+單向鏈表+紅黑樹結構(因爲如果某一個鏈表數據過長,單鏈表查詢就必須一個個遍歷,效率低,數據多的時候,把鏈表轉換爲紅黑樹,可以提高查詢效率,數據少的時候,紅黑樹會降級爲普通鏈表)
鏈表使用Node節點,紅黑樹使用TreeNode節點
圖解
核心數據結構和屬性
參數:
static final int TREEIFY_THRESHOLD = 8; |
將鏈表轉換爲紅黑樹的閾值 |
static final int UNTREEIFY_THRESHOLD = 6; |
判斷將紅黑樹轉換成鏈表的閾值 |
node
node是最核心的內部類,他包裝了key-value鍵值對
static class Node<K,V> implements Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
map本身持有一個node型的數組
transient volatile Node<K,V>[] table;
TreeNode
樹節點,當鏈表長度>=8,就會轉換成TreeNode紅黑樹
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
與1.8中HashMap不同
1、並不是直接轉換爲紅黑樹,而是把這些節點放在TreeBin對象中,有TreeBin完成對紅黑樹的包裝
2、TreeNode擴展自ConcurrentHashMap中Node類,而並非HashMap中的LinkedHashMap.Entry<K,V>類,也就是說TreeNode帶有next指針
TreeBin
負責TreeNode節點。他代替了TreeNode的根節點,也就是說在實際的ConcurrentHashMap數組中,存放的是TreeBin對象,而不是TreeNode對象,另外這個類還帶有了讀寫鎖機制。
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
特殊的ForWardingNode
一個特殊的Node節點,hash=-1,其中存儲nextTable的引用。有table發生擴容的時候,ForWardingNode發揮作用,作爲一個佔位符放在table中表示當前節點爲null或者已經被移動
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
sizeCtl:用來控制table的初始化和擴容操作
private transient volatile int sizeCtl;
負數 | 正在初始化或者擴容操作 |
-1 | 正在初始化 |
-N | 代表有N-1個線程正在進行擴容操作 |
0默認值 | 代表還沒有被初始化 |
正數 | 表示初始化大小或者Map中的元素叨叨這個數量是,需要進行擴容了 |
核心方法
/*利用硬件級別的原子操作,獲得在i位置上的Node節點
* Unsafe.getObjectVolatile可以直接獲取指定內存的數據,
* 保證了每次拿到數據都是最新的*/
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
/*利用CAS操作設置i位置上的Node節點*/
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
/*利用硬件級別的原子操作,設置在i位置上的Node節點
* Unsafe.putObjectVolatile可以直接設定指定內存的數據,
* 保證了其他線程訪問這個節點時一定可以看到最新的數據*/
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
構造方法
可以看到,值是簡單的屬性設置,並沒有初始化table,只有在put、computeIfAbsent、conpute、merge等方法的時候,檢查table==null,纔開始初始化
public ConcurrentHashMap18(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
get操作
get方法查找的時候,對於在鏈表和紅黑樹上,需要分別去查找
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());/*計算hash值*/
/*根據hash值確定節點位置*/
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
/*Node數組中的節點就是要找的節點*/
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
/*eh<0 說明這個節點在樹上 調用樹的find方法尋找*/
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
/*到這一步說明是個鏈表,遍歷鏈表找到對應的值並返回*/
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
put操作
首先根據hash值計算插入點在table中的位置i,如果i位置是空的,還沒有存放元素,直接當做鏈表節點放進去,否則要判斷,如果table【i】是紅黑樹節點,就要按照樹的方式插入新的節點,否則把i插入到鏈表的末尾。
如果是鏈表,加入元素之後,鏈表長度大於等於8,就要把鏈表裝換爲紅黑樹。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());/*計算hash值*/
int binCount = 0;
/*死循環 何時插入成功 何時跳出*/
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();/*如果table爲空的話,初始化table*/
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
/*Node數組中的元素,這個位置沒有值 ,使用CAS操作放進去*/
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
/*正在進行擴容,當前線程幫忙擴容*/
tab = helpTransfer(tab, f);
else {
V oldVal = null;
/*鎖Node數組中的元素,這個位置是Hash衝突組成鏈表的頭結點
* 或者是紅黑樹的根節點*/
synchronized (f) {
if (tabAt(tab, i) == f) {
/*fh>0 說明這個節點是一個鏈表的節點 不是樹的節點*/
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
/*put操作和putIfAbsent操作業務實現*/
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
/*如果遍歷到了最後一個結點,使用尾插法,把它插入在鏈表尾部*/
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
/*按照樹的方式插入值*/
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
/*達到臨界值8 就需要把鏈表轉換爲樹結構*/
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
/*Map的元素數量+1,並檢查是否需要擴容*/
addCount(1L, binCount);
return null;
}
initTable初始化table
在構造函數中並沒有初始化ConcurrentHashMap,初始化時發生在向map中插入元素的時候
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
/*小於0表示有其他線程正在進行初始化操作,把當前線程CPU時間讓出來。
因爲對於table的初始化工作,只能有一個線程在進行。*/
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
/*利用CAS操作把sizectl的值置爲-1 表示本線程正在進行初始化*/
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
/*n右移2位本質上就是n變爲n原值的1/4,所以
* sc=0.75*n */
sc = n - (n >>> 2);
}
} finally {
/*將設置成擴容的閾值*/
sizeCtl = sc;
}
break;
}
}
return tab;
}
多線程擴容transfer方法
併發擴容,減少擴容帶來的時間影響
1、構建一個是原來容量兩倍的newTable
2、數據從table複製到newTable
remove
移除方法基本流程和put方法類似,如果是紅黑樹,會檢查容量是不是<=6,是:紅黑樹就會轉換爲鏈表
treeifyBin
用於將過長的鏈表轉換爲TreeBin對象。但是他不是直接轉換,而是進行容量判斷,如果容量沒有達到轉化的要求。直接返回。與hashmap
不同的是他並沒有把TreeNode直接放入紅黑樹,而是利用了TreeBin這個笑容起來封裝素有的TreeNode
size
在擴容和addcount()的時候,size就已經計算好了,需要size會直接返回,這樣節省了時間。(jdk7還要實時計算才能夠得到size大小)