ConcurrentHashMap原始碼分析
本篇部落格的目錄:
一:put方法原始碼
二:get方法原始碼
三:rehash的過程
四:總結
一:put方法的原始碼
首先,我們來看一下segment內部類中put方法的原始碼,這個方法它是segment片組的,也就是我們在用concurrentHash的put方法的時候,實際上它會取得key的hashcode值,再計算它的hash,然後它會選擇一個片組,進入segment中的這個方法。所以我們根本上要看的是這個方法: public V put(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); return segmentFor(hash).put(key, hash, value, false); }
從這裡也可以看出concurrentHashMap不允許值為null,否則會丟擲NullPointetException. 複製程式碼
V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; if (c++ > threshold) // ensure capacity rehash(); HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue; if (e != null) { oldValue = e.value; if (!onlyIfAbsent) e.value = value; } else { oldValue = null; ++modCount; tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; // write-volatile } return oldValue; } finally { unlock(); } }
複製程式碼
呼叫put方法之後,它首先是lock()上鎖,防止多個執行緒同時put,可能會有併發的問題。上鎖的話可以保證每次put一個key的時候,其他執行緒將會無法進入這個片組,它會去選擇另外一個segment,這就是分段的 好處。並不簡單粗暴的採用synchronized的方法阻塞其他執行緒。接下來是取它的元素多少,給它+1(只新增一個元素)每次新加元素的時候都要去判斷它是否超過了陣列的擴容臨界值,如果超過了,就要對它進行擴容操作,也就是reHash,或者叫做"Hash再置"的過程。這裡我們先略過,暫且不分析。往下走,接下來是取得它內部的table陣列,就是封裝鍵值對的陣列,根據傳入的hash值和陣列的長度減去1進行與運算,找到一個預放置陣列的位置,然後再找它對應的陣列元素,再通過一個while迴圈去遍歷這個節點上的連結串列,去尋找這個元素,如果找到這個元素了(證明欲放入的元素已存在)。然後取得它的值,判斷onlyifAbsent,這個欄位按照字面意思翻譯為:是否缺席,也就是說放入一個元素前用這個欄位是決定它是否存在,上面的 方法傳入的引數為fasle,也就是它存在。那麼就取它的值賦值給這個元素(替換它的值)。如果它不存在,增加修改次數,然後在這個位置上新new一個元素放進去,並把加+1的值賦給count,最後再返回舊值。最後在finally裡進行解鎖。以下是圖示:
二:get方法的原始碼分析
get方法需要傳入一個key和hash。它的原理同樣等同於上面講的put方法:
public V get(Object key) {//根據key獲取value int hash = hash(key.hashCode());//拿到鍵的hash值 return segmentFor(hash).get(key, hash);//呼叫segmentFor方法傳入key和hash值得到value }
通過key的hashcode值,傳入segment中的get方法: 複製程式碼
V get(Object key, int hash) { //根據指定的key和hash值獲取value值 if (count != 0) { // 如果count不為0 HashEntry<K,V> e = getFirst(hash);//根據傳入的hash獲取連結串列中的第一個鍵值對 while (e != null) {//如果這個鍵值對不為null if (e.hash == hash && key.equals(e.key)) {//如果該鍵值對的hash值等於方法傳入的Hash,並且該鍵與第一個Hashentry物件通過equals方法比較相同 V v = e.value;//取第一個hashEntry物件的值 if (v != null)//如果該值不為null return v;//返回值 return readValueUnderLock(e); // 呼叫readValueUnderLock方法返回物件的值 } e = e.next;//指向下一個鍵值對,這裡相當於去遍歷整個連結串列,直到找到key對應的值 } } return null;//如果找不到,返回null }
複製程式碼
get方法首先判斷的是陣列中的元素是不是0,如果不是0繼續往下走,然後通過傳入的hash值去獲取他的第一個元素,如果這個元素不為null,說明可以找到這個hash對應的元素。否則就返回null。然後通過while迴圈再去判斷hash值是否相同,key是否相同,在兩者相同的情況下,獲取該元素的value。如果這個value不為null,就返回這個值。如果它為null,呼叫readValueUnderLock()方法,這裡主要是考慮到一點,如果再它取值的過程中,如果這個值正在被put進去。再來看看readValueUnderLock方法: 複製程式碼
V readValueUnderLock(HashEntry<K,V> e) {//在鎖中讀取指定的HashEntry值 lock();//上鎖 try { return e.value;//返回Hashentry中的value } finally { unlock();//解鎖 } }
複製程式碼
這裡專門做了一個上鎖的過程,主要是為了防止獲取值的過程這個值正在被新增,此刻就會對取值進行上鎖,那麼put方法就會被阻塞,只得等它get完畢再put,那麼又會有一個新的問題產生:比如假如一個執行緒現在要put一個鍵值對:put(“a”,“sunday”),而map裡面已經存在一個“a”,“Monday”;而另外一個執行緒正在get(“a”),此時得到的值是null還是“sunday”,還是monday?回答這個問題,只需要看這裡transient volatile HashEntry<K,V>[] table;table是volatile的,所以它可以及時的同步它的Hashentry,它可以保證取到最後一次put的值。
三:rehash的過程
rehash的過程就是擴容的過程,每次要put一個值的時候,都要呼叫這個方法給當前的容量+1去檢查是不是超過最大容量。我們來看一下它的原始碼,分析一下這個過程: 複製程式碼
void rehash() HashEntry<K,V>[] oldTable = table; //取當前的陣列設為舊陣列 int oldCapacity = oldTable.length;//取舊陣列的陣列的長度 if (oldCapacity >= MAXIMUM_CAPACITY)//判斷舊陣列的容量是否大於最大容量(保證當前的陣列不越界) return;//如果是 結束 HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1);//以舊陣列的長度的2倍建立一個新陣列 threshold = (int)(newTable.length * loadFactor);//設定臨界值為新陣列的長度乘以載入因子 int sizeMask = newTable.length - 1;//設定大小的掩碼為新陣列的長度減去1 for (int i = 0; i < oldCapacity ; i++) {//遍歷舊陣列,也就是複製陣列的過程 // We need to guarantee that any existing reads of old Map can // proceed. So we cannot yet null out each bin. HashEntry<K,V> e = oldTable[i];//取陣列的元素
if (e != null) {//如果不為null
HashEntry<K,V> next = e.next;//往下遍歷該節點上的連結串列
int idx = e.hash & sizeMask;//用該節點的hash乘以大小的掩碼獲取一個位置值
// Single node on list
if (next == null)//如果該節點上沒有形成連結串列
newTable[idx] = e;//把新該元素的值設為新陣列的計算出來的位置的值
else { //如果該節點有連續的連結串列
// Reuse trailing consecutive sequence at same slot
HashEntry<K,V> lastRun = e;//取該節點
int lastIdx = idx;//取計算出來的位置
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {//遍歷該連結串列
int k = last.hash & sizeMask;//通過該元素的hash值與size掩碼進行與運算出來一個位置值
if (k != lastIdx) {//如果兩個值不相同
lastIdx = k;//把k的值賦值給lastIdx
lastRun = last;//把當前值設為lastRun的值
}
}
newTable[lastIdx] = lastRun;//用得出的值賦值給新陣列
// Clone all remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {//遍歷迴圈該連結串列中的元素
int k = p.hash & sizeMask;//取元素的hash值與size掩碼進行與運算計算出它的位置
HashEntry<K,V> n = newTable[k];//取計算出來的位置元素的值
newTable[k] = new HashEntry<K,V>(p.key, p.hash,
n, p.value);//呼叫HashEntry的構造方法新建一個新HashEntry物件
}
}
}
}
table = newTable;//把新陣列設定為片組維持的table
}
複製程式碼
上面這個方法主要是對陣列擴容的過程做一個簡單的分析,根據程式碼可以發現以下幾點問題:
1:陣列擴容的時候是把原陣列的長度*2(左移1位)
2:然後去遍歷舊陣列,這裡分為兩種情況,舊陣列的節點存在連結串列和不存在連結串列,如果不存在連結串列,會把當前節點的hash與它的index進行與運算得出一個位置,然後把它放入到新素組的該位置
3:如果存在連結串列:會遍歷當前的連結串列,然後把舊陣列的當前值設為新陣列計算出來的值後,再遍歷該連結串列,把連結串列裡面的值的key和value還有index位置新構建一個元素放入到新陣列中
4:最後再把這個新陣列代替原來的陣列,讓segment維護這個新陣列