日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

【本人秃顶程序员】深入理解Java——ConcurrentHashMap源码的分析(JDK1.8)

發(fā)布時間:2025/3/15 java 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【本人秃顶程序员】深入理解Java——ConcurrentHashMap源码的分析(JDK1.8) 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

←←←←←←←←←←←← 快!點關注

一、前提

在閱讀這篇博客之前,希望你對HashMap已經(jīng)是有所理解的,如果你對java的cas操作也是有一定了解的,因為在這個類中大量使用到了cas相關的操作來保證線程安全的。

二、概述

ConcurrentHashMap這個類在java.lang.current包中,這個包中的類都是線程安全的。ConcurrentHashMap底層存儲數(shù)據(jù)的結構與1.8的HashMap是一樣的,都是數(shù)組+鏈表(或紅黑樹)的結構。在日常的開發(fā)中,我們最長用到的鍵值對存儲結構的是HashMap,但是我們知道,這個類是非線程安全的,在高并發(fā)的場景下,在進行put操作的時候有可能進入死循環(huán)從而使服務器的cpu使用率達到100%;sun公司因此也給出了與之對應的線程安全的類。在jdk1.5以前,使用的是HashTable,這個類為了保證線程安全,在每個類中都添加了synchronized關鍵字,而想而知在高并發(fā)的情景下相率是非常低下的。為了解決HashTable效率低下的問題,官網(wǎng)在jdk1.5后推出了ConcurrentHashMap來替代飽受詬病的HashTable。jdk1.5后ConcurrentHashMap使用了分段鎖的技術。在整個數(shù)組中被分為多個segment,每次get,put,remove操作時就鎖住目標元素所在的segment中,因此segment與segment之前是可以并發(fā)操作的,上述就是jdk1.5后實現(xiàn)線程安全的大致思想。但是,從描述中可以看出一個問題,就是如果出現(xiàn)比較機端的情況,所有的數(shù)據(jù)都集中在一個segment中的話,在并發(fā)的情況下相當于鎖住了全表,這種情況下其實是和HashTable的效率出不多的,但總體來說相較于HashTable,效率還是有了很大的提升。jdk1.8后,ConcurrentHashMap摒棄了segment的思想,轉而使用cas+synchronized組合的方式來實現(xiàn)并發(fā)下的線程安全的,這種實現(xiàn)方式比1.5的效率又有了比較大的提升。那么,它是如何整體提升效率的呢?見下文分析吧!

三、重要成員變量

1. ziseCtr:在多個方法中出現(xiàn)過這個變量,該變量主要是用來控制數(shù)組的初始化和擴容的,默認值為0,可以概括一下4種狀態(tài):

  • a、sizeCtr=0:默認值;
  • b、sizeCtr=-1:表示Map正在初始化中;
  • c、sizeCtr=-N:表示正在有N-1個線程進行擴容操作;
  • d、sizeCtr>0: 未初始化則表示初始化Map的大小,已初始化則表示下次進行擴容操作的閾值;

2. table:用于存儲鏈表或紅黑數(shù)的數(shù)組,初始值為null,在第一次進行put操作的時候進行初始化,默認值為16; 3. nextTable:在擴容時新生成的數(shù)組,其大小為當前table的2倍,用于存放table轉移過來的值; 4. Node:該類存儲數(shù)據(jù)的核心,以key-value形式來存儲; 5. ForwardingNode:這是一個特殊Node節(jié)點,僅在進行擴容時用作占位符,表示當前位置已被移動或者為null,該node節(jié)點的hash值為-1;

四、put操作

先把源碼擺上來:

/** Implementation for put and putIfAbsent */ final V putVal(K key, V value, Boolean onlyIfAbsent) {//key和value不能為空if (key == null || value == null) throw new NullPointerException();//通過key來計算獲得hash值int hash = spread(key.hashCode());//用于計算數(shù)組位置上存放的node的節(jié)點數(shù)量//在put完成后會對這個參數(shù)判斷是否需要轉換成紅黑樹或鏈表int binCount = 0;//使用自旋的方式放入數(shù)據(jù)//這個過程是非阻塞的,放入失敗會一直循環(huán)嘗試,直至成功for (Node<K,V>[] tab = table;;) {Node<K,V> f;int n, i, fh;//第一次put操作,對數(shù)組進行初始化,實現(xiàn)懶加載if (tab == null || (n = tab.length) == 0)//初始化tab = initTable();//數(shù)組已初始化完成后//使用cas來獲取插入元素所在的數(shù)組的下標的位置,該位置為空的話就直接放進去 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;// no lock when adding to empty bin}//hash=-1,表明該位置正在進行擴容操作,讓當前線程也幫助該位置上的擴容,并發(fā)擴容提高擴容的速度 else if ((fh = f.hash) == MOVED)//幫助擴容tab = helpTransfer(tab, f);//插入到該位置已有數(shù)據(jù)的節(jié)點上,即用hash沖突//在這里為保證線程安全,會對當前數(shù)組位置上的第一個節(jié)點進行加鎖,因此其他位置上//仍然可以進行插入,這里就是jdk1.8相較于之前版本使用segment作為鎖性能要高效的地方 else {V oldVal = null;synchronized (f) {//再一次判斷f節(jié)點是否為第一個節(jié)點,防止其他線程已修改f節(jié)點if (tabAt(tab, i) == f) {//為鏈表if (fh >= 0) {binCount = 1;//將節(jié)點放入鏈表中for (Node<K,V> e = f;; ++binCount) {K ek;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;//將節(jié)點插入紅黑樹中if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}//插入成功后判斷插入數(shù)據(jù)所在位置上的節(jié)點數(shù)量,//如果數(shù)量達到了轉化紅黑樹的閾值,則進行轉換if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)//由鏈表轉換成紅黑樹treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}//使用cas統(tǒng)計數(shù)量增加1,同時判斷是否滿足擴容需求,進行擴容addCount(1L, binCount);return null; } 復制代碼

在代碼上寫注釋可能看得不是很清晰,那么我就使用文字再來描述一下插入數(shù)據(jù)的整個流程:

  • 判斷傳進來的key和value是否為空,在ConcurrentHashMap中key和value都不允許為空,然而在HashMap中是可以為key和val都可以為空,這一點值得注意一下;
  • 對key進行重hash計算,獲得hash值;
  • 如果當前的數(shù)組為空,說明這是第一插入數(shù)據(jù),則會對table進行初始化;
  • 插入數(shù)據(jù),這里分為3中情況: 1). 插入位置為空,直接將數(shù)據(jù)放入table的第一個位置中; 2). 插入位置不為空,并且改為是一個ForwardingNode節(jié)點,說明該位置上的鏈表或紅黑樹正在進行擴容,然后讓當前線程加進去并發(fā)擴容,提高效率; 3). 插入位置不為空,也不是ForwardingNode節(jié)點,若為鏈表則從第一節(jié)點開始組個往下遍歷,如果有key的hashCode相等并且值也相等,那么就將該節(jié)點的數(shù)據(jù)替換掉,否則將數(shù)據(jù)加入到鏈表末段;若為紅黑樹,則按紅黑樹的規(guī)則放進相應的位置;
  • 數(shù)據(jù)插入成功后,判斷當前位置上的節(jié)點的數(shù)量,如果節(jié)點數(shù)據(jù)大于轉換紅黑樹閾值(默認為8),則將鏈表轉換成紅黑樹,提高get操作的速度;
  • 數(shù)據(jù)量+1,并判斷當前table是否需要擴容;
  • 所以,put操作流程可以簡單的概括為上面的六個步驟,其中一些具體的操作會在下面進行詳細的說明,不過,值得注意的是:

    • ConcurrentHashMap不可以存儲key或value為null的數(shù)據(jù),有別于HashMap;
    • ConcurrentHashMap使用了懶加載的方式初始化數(shù)據(jù),把table的初始化放在第一次put數(shù)據(jù)的時候,而不是在new的時候;
    • 擴容時是支持并發(fā)擴容,這將有助于減少擴容的時間,因為每次擴容都需要對每個節(jié)點進行重hash,從一個table轉移到新的table中,這個過程會耗費大量的時間和cpu資源。
    • 插入數(shù)據(jù)操作鎖住的是表頭,這是并發(fā)效率高于jdk1.7的地方;

    Ⅰ、hash計算的spread方法

    /*** Spreads (XORs) higher bits of hash to lower and also forces top* bit to 0. Because the table uses power-of-two masking, sets of* hashes that vary only in bits above the current mask will* always collide. (Among known examples are sets of Float keys* holding consecutive whole numbers in small tables.) So we* apply a transform that spreads the impact of higher bits* downward. There is a tradeoff between speed, utility, and* quality of bit-spreading. Because many common sets of hashes* are already reasonably distributed (so don't benefit from* spreading), and because we use trees to handle large sets of* collisions in bins, we just XOR some shifted bits in the* cheapest possible way to reduce systematic lossage, as well as* to incorporate impact of the highest bits that would otherwise* never be used in index calculations because of table bounds.*/ static final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS; } 復制代碼

    從源碼中可以看到,jdk1.8計算hash的方法是先獲取到key的hashCode,然后對hashCode進行高16位和低16位異或運算,然后再與 0x7fffffff 進行與運算。高低位異或運算可以保證haahCode的每一位都可以參與運算,從而使運算的結果更加均勻的分布在不同的區(qū)域,在計算table位置時可以減少沖突,提高效率,我們知道Map在put操作時大部分性能都耗費在解決hash沖突上面。得出運算結果后再和 0x7fffffff 與運算,其目的是保證每次運算結果都是一個正數(shù)。對于java位運算不了解的同學,建議百度自行了解相關內(nèi)容。

    Ⅱ、java內(nèi)存模型和cas操作

    這里我只是簡單的說一下java的內(nèi)存模型和cas,因為這篇文章的主角的ConcurrentHashMap。

    java內(nèi)存模型:在java中線程之間的通訊是通過共享內(nèi)存(即我們在變成時聲明的成員變量或叫全局變量)的來實現(xiàn)的。Java內(nèi)存模型中規(guī)定了所有的變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存(可以與前面將的處理器的高速緩存類比),線程的工作內(nèi)存中保存了該線程使用到的變量到主內(nèi)存副本拷貝,線程對變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進行,而不能直接讀寫主內(nèi)存中的變量。不同線程之間無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要在主內(nèi)存來完成,線程、主內(nèi)存和工作內(nèi)存的交互關系如下圖所示,和上圖很類似。

    舉一個非常簡單的例子,就是我們常用的i++的操作,這個操作看起來只有一行,然而在編譯器中這一行代碼會被編譯成3條指令,分別是讀取、更新和寫入,所以i++并不是一個原子操作,在多線程環(huán)境中是有問題了。其原因在于(我們假設當前 i 的值為1)當一條線程向主內(nèi)存中讀取數(shù)據(jù)時,還沒來得及把更新后的值刷新到主內(nèi)存中,另一個線程就已經(jīng)開始向主內(nèi)存中讀取了數(shù)據(jù),而此時內(nèi)存中的值仍然為1,兩個線程執(zhí)行+1操作后得到的結果都為2,然后將結果刷新到主內(nèi)存中,整個i++操作結果,最終得到的結果為2,但是我們預想的結果應該是3,這就出現(xiàn)了線程安全的問題了。

    cas: cas的全名稱是Compare And Swap 即比較交換。cas算法在不需要加鎖的情況也可以保證多線程安全。核心思想是: cas中有三個變量,要更新的變量V,預期值E和新值N,首先先讀取V的值,然后進行相關的操作,操作完成后再向主存中讀取一次取值為E,當且僅當V == E時才將N賦值給V,否則再走一遍上訴的流程,直至更新成功為止。就拿上面的i++的操作來做說明,假設當前i=1,兩個線程同時對i進行+1的操作,線程A中V = 1,E = 1,N = 2;線程B中 V = 1,E = 1,N = 2;假設線程A先執(zhí)行完整個操作,此時線程A發(fā)現(xiàn) V = E = 1,所以線程A將N的值賦值給V,那么此時i的值就變成了 2 ;線程B隨后也完成了操作,向主存中讀取i的值,此時E = 2,V = 1,V != E,發(fā)現(xiàn)兩個并不相等,說明i已經(jīng)被其他線程修改了,因此不執(zhí)行更新操作,而是從新讀取V的值V = 2 ,執(zhí)行+1后N = 3,完成后再讀取主存中i的值,因為此時沒有其他線程修改i的值了,所以E = 2,V = E = 2,兩個值相等,因此執(zhí)行賦值操作,將N的值賦值給i,最終得到的結果為3。在整過過程中始終沒有使用到鎖,卻實現(xiàn)的線程的安全性。

    從上面的過程知道,cas會面臨著兩個問題,一個是當線程一直更新不成功的話,那么這個線程就一直處于死循環(huán)中,這樣會非常耗費cpu的資源;另一種是ABA的問題,即對i =1進行+1操作后,再-1,那么此時i的值仍為1,而另外一個線程獲取的E的值也是1,認為其他線程沒有修改過i,然后進行的更新操作,事實上已經(jīng)有其他線程修改過了這個值了,這個就是 A ---> B ---> A 的問題;

    Ⅲ、獲取table對應的索引元素的位置

    通過(n-1)& hash 的算法來獲得對應的table的下標的位置,如果對于這條公式不是很理解的同學可以到: jdk1.8源碼分析-hashMap 博客中了解。

    tabAt(Node<K,V>[] tab, int i): 這個方法使用了java提供的原子操作的類來操作的,sun.misc.Unsafe.getObjectVolatile 的方法來保證每次線程都能獲取到最新的值;

    casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v): 這個方法是通過cas的方式來獲取i位置的元素;

    Ⅳ、擴容

    • 如果新增節(jié)點之后,所在的鏈表的元素個數(shù)大于等于8,則會調(diào)用treeifyBin把鏈表轉換為紅黑樹。在轉換結構時,若tab的長度小于MIN_TREEIFY_CAPACITY,默認值為64,則會將數(shù)組長度擴大到原來的兩倍,并觸發(fā)transfer,重新調(diào)整節(jié)點位置。(只有當tab.length >= 64, ConcurrentHashMap才會使用紅黑樹。)

    • 新增節(jié)點后,addCount統(tǒng)計tab中的節(jié)點個數(shù)大于閾值(sizeCtl),會觸發(fā)transfer,重新調(diào)整節(jié)點位置。

    /*** Adds to count, and if table is too small and not already* resizing, initiates transfer. If already resizing, helps* perform transfer if work is available. Rechecks occupancy* after a transfer to see if another resize is already needed* because resizings are lagging additions.** @param x the count to add* @param check if <0, don't check resize, if <= 1 only check if uncontended*/ private final void addCount(long x, int check) {CounterCell[] as;long b, s;if ((as = counterCells) != null ||!U.compareAndSwaplong(this, BASECOUNT, b = baseCount, s = b + x)) {CounterCell a;long v;int m;Boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[ThreadLocalRandom.getProbe() & m]) == null ||!(uncontended =U.compareAndSwaplong(a, CELLVALUE, v = a.value, v + x))) {fullAddCount(x, uncontended);return;}if (check <= 1)return;s = sumCount();}if (check >= 0) {Node<K,V>[] tab, nt;int n, sc;while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY) {int rs = resizeStamp(n);if (sc < 0) {if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;if (U.compareAndSwapint(this, SIZECTL, sc, sc + 1))transfer(tab, nt);} else if (U.compareAndSwapint(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))transfer(tab, null);s = sumCount();}} } 復制代碼

    五、get操作

    get操作中沒有使用到同步的操作,所以相對來說比較簡單一點。通過key的hashCode計算獲得相應的位置,然后在遍歷該位置上的元素,找到需要的元素,然后返回,如果沒有則返回null:

    /*** Returns the value to which the specified key is mapped,* or {@code null} if this map contains no mapping for the key.** <p>More formally, if this map contains a mapping from a key* {@code k} to a value {@code v} such that {@code key.equals(k)},* then this method returns {@code v}; otherwise it returns* {@code null}. (There can be at most one such mapping.)** @throws NullPointerException if the specified key is null*/ public V get(Object key) {Node<K,V>[] tab;Node<K,V> e, p;int n, eh;K ek;int h = spread(key.hashCode());if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {if ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;} 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; } 復制代碼

    轉載于:https://juejin.im/post/5cc06d33e51d456e403772e5

    總結

    以上是生活随笔為你收集整理的【本人秃顶程序员】深入理解Java——ConcurrentHashMap源码的分析(JDK1.8)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。