併發包裏面ConcurrentHashMap是一個使用頻率,面試頻率都是用到比較高的。今天咱們就剖析一下ConCurrentHashMap的源碼。咱們仍是使用場景驅動的方式去分析。java
可是發現構造函數進去之後就是一個無參的構造,沒什麼特別的。node
2.分析put流程面試
put流程是整個ConCurrentHashMap的精華部分,裏面淋漓盡致的使用了分段加鎖的思想。緩存
3.分析get流程安全
Get是一個讀流程,採用的也是無鎖化方式,可是方法調的底層的unsafe的方法保證了可見性。整個ConCurrentHashMap的性能很高。併發
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>(); //剖析寫數據流程 map.put("test","zhangsan"); //剖析讀數據流程 map.get("test") 接着咱們分析一下put的方法,這個方法很是重要。體現了整個ConCurrentHashMap的設計精髓。Put點進去之後是這個方法 public V put(K key, V value) { //重要,點進去之後繼續分析 return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { //若是key或者value其中一個null,就報空指針異常 if (key == null || value == null) throw new NullPointerException(); //取hash值,可是咱們注意的是,這兒並非直接用的key的hashCode值。 //而是調用了spred方法。 //spread裏的關鍵代碼是(h ^ (h >>> 16)) & HASH_BITS; //用當前的hashCode值向右移了16位,而後再作異或 //這樣操做的出來的值都保留着着hashCode的高低16位的特徵,減小hash衝突 //因此咱們這兒要知道。這兒的hash值不是直接用的hashCode int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //若是當前的tab裏面沒有數據 if (tab == null || (n = tab.length) == 0) //初始化table,而後進去下一輪循環 tab = initTable(); //進入下一輪循環之後走到這兒。 //return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); //這兒就是計算index的位置,使用tabAt的方法查找對應位置的數據 //若是對應位置的數據是null,說明咱們要插入的這個位置目前尚未數據 //注意這兒使用的是unsafe的getObjectVolatile,也就是說,這個是具備可見性的 //若是併發的時候有人修改了這個值,對這個操做也是課件。 //若是有同窗看過我以前分析過《從硬件層面聊聊synchonzied和volatile》 //應該知道volatile這兒至關因而加了load屏障,在獲取數據的時候,若是在無效隊列裏面發 //現數據無效,就會從新加載。 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //若是當前位置沒有元素,直接就經過cas進行插入元素。 //cas操做的代碼是 //return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); //其實這個cas操做在硬件層面是加鎖的,因此這兒實際上是線程安全的。這點你們必定 //要知道。 //那麼這兒咱們還能夠繼續分析一下,若是這兒同時兩個線程併發進來操做 //必然有一個成功的,一個是失敗了,成功的,固然就添加了數據了。 //其實這個地方分段加鎖的思想已經出來了,由於咱們分析指導 //針對表的一個位置,咱們採用的cas的機制,這個時候,只有針對同一個位置 //纔會插入失敗。可是其他的位置照樣能夠進行cas的操做。換句話說一個位置一把鎖 //由於是for循環,因此失敗了的那個確定再次循環。 //可是由於這個位置已經有數據了。因此代碼不走這兒了。 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 //其實這兒也就體現了分段加鎖的思想。一個Node就一把鎖 synchronized (f) { //f其實就是前面要加鎖的時候,獲取出來的node //其實若是代碼走到這兒,表明的就是tabAt(tab, i) == f //可是這兒還要繼續判斷就是爲了一個嚴謹 //由於就怕中間有人修改過這個f這個node if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; //若是插入的key和value //key是同樣的而且value不爲空 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; //直接就覆蓋 if (!onlyIfAbsent) e.val = value; break; } //代碼走到這兒,只能說明是 //key不同,只不過是巧了,雖然key不同 //可是通過hash運算之後,當前key定位了一個 //已經有數據的位置上。 Node<K,V> pred = e; if ((e = e.next) == null) { //直接就在當前node上掛載 一個node節點 //注意,一個節點上掛載的節點超過8個 //就會變化爲紅黑樹 pred.next = new Node<K,V>(hash, key, value, null); //掛載成功之後就退出 break; } } } //若是當前定位到的Node已經變成是紅黑樹 //那麼就選擇使用紅黑樹的方式插入數據 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; } } } //計算node上掛載的節點個數 addCount(1L, binCount); return null; } private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin 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; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; } 上面咱們分析了寫入數據的時候加鎖的思想,接下來咱們分析一下,ConcurrentHashMap 讀數據的時候又是什麼狀況? public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; //計算位置 int h = spread(key.hashCode()); //這個條件判斷,若是table裏是有數據的,而且根據當前 //key計算出來的位置也是有數據的 if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { //若是是hash值相同 if ((eh = e.hash) == h) { //若是是key相同 if ((ek = e.key) == key || (ek != null && key.equals(ek))) //那麼咱們直接返回當前的value值就行。 return e.val; } else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; //若是代碼走到這兒說明,要獲取的key的對應 //hash位置確實是有數據的,對應位置的key跟傳 //進來的key位置不同,因此遍歷列表 while ((e = e.next) != null) { //直到遍歷到 hash位置同樣,key也同樣位置。 //而後就把當前value值返回便可。 if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; } 整個讀的操做,咱們分析實際上是都沒有加鎖的。 不過咱們注意到map裏的元素咱們是靠tabAt(tab, (n - 1) & h)這個方法獲取到的。 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { //調用了Unsafe的方法 //還保證了可見性。 return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); }
讀這個操做保證了可見性,從硬件層面說,就是加了load屏障。因此在讀數據的時候必定會嗅探一下,若是發現無效隊列裏面元素被人修改過。就會發送read消息到總線。從別的高速緩存裏或者是主內存裏讀取數據。不加鎖,可是他經過volatile讀,儘量給你保證讀到其餘線程修改的最新的值。函數