最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java ConcurrentHashMap用法案例詳解

 更新時間:2021年08月12日 09:07:34   作者:光何  
這篇文章主要介紹了Java ConcurrentHashMap用法案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下

一、概念

哈希算法(hash algorithm):是一種將任意內(nèi)容的輸入轉(zhuǎn)換成相同長度輸出的加密方式,其輸出被稱為哈希值。

哈希表(hash table):根據(jù)設(shè)定的哈希函數(shù)H(key)和處理沖突方法將一組關(guān)鍵字映象到一個有限的地址區(qū)間上,并以關(guān)鍵字在地址區(qū)間中的象作為記錄在表中的存儲位置,這種表稱為哈希表或散列,所得存儲位置稱為哈希地址或散列地址。

二、HashMap與HashTable

1,線程不安全的HashMap

  因為多線程環(huán)境下,使用HashMap進行put操作會引起死循環(huán),導(dǎo)致CPU利用率接近100%,所以在并發(fā)情況下不能使用HashMap,如以下代碼

final HashMap<String, String> map = new HashMap<String, String>(2);
Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    map.put(UUID.randomUUID().toString(), "");
                }
            }, "ftf" + i).start();
        }
    }
}, "ftf");
t.start();
t.join();

2,效率低下的HashTable容器

  HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下。因為當(dāng)一個線程訪問HashTable的同步方法時,其他線程訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態(tài)。如線程1使用put進行添加元素,線程2不但不能使用put方法添加元素,并且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。

三、ConcurrentHashMap

1,鎖分段技術(shù)

  HashTable容器在競爭激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因是所有訪問HashTable的線程都必須競爭同一把鎖,那假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問容器里不同數(shù)據(jù)段的數(shù)據(jù)時,線程間就不會存在鎖競爭,從而可以有效的提高并發(fā)訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術(shù),首先將數(shù)據(jù)分成一段一段的存儲,然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候,其他段的數(shù)據(jù)也能被其他線程訪問。

2,ConcurrentHashMap的結(jié)構(gòu)

  我們通過ConcurrentHashMap的類圖來分析ConcurrentHashMap的結(jié)構(gòu)。ConcurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap里扮演鎖的角色,HashEntry則用于存儲鍵值對數(shù)據(jù)。一個ConcurrentHashMap里包含一個Segment數(shù)組,Segment的結(jié)構(gòu)和HashMap類似,是一種數(shù)組和鏈表結(jié)構(gòu), 一個Segment里包含一個HashEntry數(shù)組,每個HashEntry是一個鏈表結(jié)構(gòu)的元素, 每個Segment守護者一個HashEntry數(shù)組里的元素,當(dāng)對HashEntry數(shù)組的數(shù)據(jù)進行修改時,必須首先獲得它對應(yīng)的Segment鎖。

3,ConcurrentHashMap的初始化

  ConcurrentHashMap初始化方法是通過initialCapacity,loadFactor, concurrencyLevel幾個參數(shù)來初始化segments數(shù)組,段偏移量segmentShift,段掩碼segmentMask和每個segment里的HashEntry數(shù)組 。

  初始化segments數(shù)組。讓我們來看一下初始化segmentShift,segmentMask和segments數(shù)組的源代碼。

if (concurrencyLevel > MAX_SEGMENTS)
    concurrencyLevel = MAX_SEGMENTS;

// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
    ++sshift;
    ssize <<= 1;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);

由上面的代碼可知segments數(shù)組的長度ssize通過concurrencyLevel計算得出。為了能通過按位與的哈希算法來定位segments數(shù)組的索引,必須保證segments數(shù)組的長度是2的N次方(power-of-two size),所以必須計算出一個是大于或等于concurrencyLevel的最小的2的N次方值來作為segments數(shù)組的長度。假如concurrencyLevel等于14,15或16,ssize都會等于16,即容器里鎖的個數(shù)也是16。注意concurrencyLevel的最大大小是65535,意味著segments數(shù)組的長度最大為65536,對應(yīng)的二進制是16位。

  初始化segmentShift和segmentMask。這兩個全局變量在定位segment時的哈希算法里需要使用,sshift等于ssize從1向左移位的次數(shù),在默認(rèn)情況下concurrencyLevel等于16,1需要向左移位移動4次,所以sshift等于4。segmentShift用于定位參與hash運算的位數(shù),segmentShift等于32減sshift,所以等于28,這里之所以用32是因為ConcurrentHashMap里的hash()方法輸出的最大數(shù)是32位的,后面的測試中我們可以看到這點。segmentMask是哈希運算的掩碼,等于ssize減1,即15,掩碼的二進制各個位的值都是1。因為ssize的最大長度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,對應(yīng)的二進制是16位,每個位都是1。

  初始化每個Segment。輸入?yún)?shù)initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每個segment的負(fù)載因子,在構(gòu)造方法里需要通過這兩個參數(shù)來初始化數(shù)組中的每個segment。

if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
    ++c;
int cap = 1;
while (cap < c)
    cap <<= 1;
for (int i = 0; i < this.segments.length; ++i)
    this.segments[i] = new Segment<K,V>(cap, loadFactor);

上面代碼中的變量cap就是segment里HashEntry數(shù)組的長度,它等于initialCapacity除以ssize的倍數(shù)c,如果c大于1,就會取大于等于c的2的N次方值,所以cap不是1,就是2的N次方。segment的容量threshold=(int)cap*loadFactor,默認(rèn)情況下initialCapacity等于16,loadfactor等于0.75,通過運算cap等于1,threshold等于零。

4,定位Segment

  既然ConcurrentHashMap使用分段鎖Segment來保護不同段的數(shù)據(jù),那么在插入和獲取元素的時候,必須先通過哈希算法定位到Segment??梢钥吹紺oncurrentHashMap會首先使用Wang/Jenkins hash的變種算法對元素的hashCode進行一次再哈希。

rivate static int hash(int h) {
        h += (h << 15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h << 3);
        h ^= (h >>> 6);
        h += (h << 2) + (h << 14);
        return h ^ (h >>> 16);
    }

之所以進行再哈希,其目的是為了減少哈希沖突,使元素能夠均勻的分布在不同的Segment上,從而提高容器的存取效率。假如哈希的質(zhì)量差到極點,那么所有的元素都在一個Segment中,不僅存取元素緩慢,分段鎖也會失去意義。我做了一個測試,不通過再哈希而直接執(zhí)行哈希計算。

1 System.out.println(Integer.parseInt("0001111", 2) & 15);
2 System.out.println(Integer.parseInt("0011111", 2) & 15);
3 System.out.println(Integer.parseInt("0111111", 2) & 15);
4 System.out.println(Integer.parseInt("1111111", 2) & 15);

  計算后輸出的哈希值全是15,通過這個例子可以發(fā)現(xiàn)如果不進行再哈希,哈希沖突會非常嚴(yán)重,因為只要低位一樣,無論高位是什么數(shù),其哈希值總是一樣。我們再把上面的二進制數(shù)據(jù)進行再哈希后結(jié)果如下,為了方便閱讀,不足32位的高位補了0,每隔四位用豎線分割下。

1 0100|0111|0110|0111|1101|1010|0100|1110
2 1111|0111|0100|0011|0000|0001|1011|1000
3 0111|0111|0110|1001|0100|0110|0011|1110
4 1000|0011|0000|0000|1100|1000|0001|1010

可以發(fā)現(xiàn)每一位的數(shù)據(jù)都散列開了,通過這種再哈希能讓數(shù)字的每一位都能參加到哈希運算當(dāng)中,從而減少哈希沖突。ConcurrentHashMap通過以下哈希算法定位segment。

1 final Segment<K,V> segmentFor(int hash) {
2         return segments[(hash >>> segmentShift) & segmentMask];
3     }

默認(rèn)情況下segmentShift為28,segmentMask為15,再哈希后的數(shù)最大是32位二進制數(shù)據(jù),向右無符號移動28位,意思是讓高4位參與到hash運算中, (hash >>> segmentShift) & segmentMask的運算結(jié)果分別是4,15,7和8,可以看到hash值沒有發(fā)生沖突。

5,ConcurrentHashMap的get操作

  Segment的get操作實現(xiàn)非常簡單和高效。先經(jīng)過一次再哈希,然后使用這個哈希值通過哈希運算定位到segment,再通過哈希算法定位到元素,代碼如下:

1 public V get(Object key) {
2     int hash = hash(key.hashCode());
3     return segmentFor(hash).get(key, hash);
4 }

get操作的高效之處在于整個get過程不需要加鎖,除非讀到的值是空的才會加鎖重讀,我們知道HashTable容器的get方法是需要加鎖的,那么ConcurrentHashMap的get操作是如何做到不加鎖的呢?原因是它的get方法里將要使用的共享變量都定義成volatile,如用于統(tǒng)計當(dāng)前Segement大小的count字段和用于存儲值的HashEntry的value。定義成volatile的變量,能夠在線程之間保持可見性,能夠被多線程同時讀,并且保證不會讀到過期的值,但是只能被單線程寫(有一種情況可以被多線程寫,就是寫入的值不依賴于原值),在get操作里只需要讀不需要寫共享變量count和value,所以可以不用加鎖。之所以不會讀到過期的值,是根據(jù)java內(nèi)存模型的happen before原則,對volatile字段的寫入操作先于讀操作,即使兩個線程同時修改和獲取volatile變量,get操作也能拿到最新的值,這是用volatile替換鎖的經(jīng)典應(yīng)用場景。

1 transient volatile int count;
2 volatile V value;

  在定位元素的代碼里我們可以發(fā)現(xiàn)定位HashEntry和定位Segment的哈希算法雖然一樣,都與數(shù)組的長度減去一相與,但是相與的值不一樣,定位Segment使用的是元素的hashcode通過再哈希后得到的值的高位,而定位HashEntry直接使用的是再哈希后的值。其目的是避免兩次哈希后的值一樣,導(dǎo)致元素雖然在Segment里散列開了,但是卻沒有在HashEntry里散列開。

1 hash >>> segmentShift) & segmentMask//定位Segment所使用的hash算法
2 int index = hash & (tab.length - 1);// 定位HashEntry所使用的hash算法

6,ConcurrentHashMap的Put操作

  由于put方法里需要對共享變量進行寫入操作,所以為了線程安全,在操作共享變量時必須得加鎖。Put方法首先定位到Segment,然后在Segment里進行插入操作。插入操作需要經(jīng)歷兩個步驟,第一步判斷是否需要對Segment里的HashEntry數(shù)組進行擴容,第二步定位添加元素的位置然后放在HashEntry數(shù)組里。

  是否需要擴容。在插入元素前會先判斷Segment里的HashEntry數(shù)組是否超過容量(threshold),如果超過閥值,數(shù)組進行擴容。值得一提的是,Segment的擴容判斷比HashMap更恰當(dāng),因為HashMap是在插入元素后判斷元素是否已經(jīng)到達(dá)容量的,如果到達(dá)了就進行擴容,但是很有可能擴容之后沒有新元素插入,這時HashMap就進行了一次無效的擴容。

  如何擴容。擴容的時候首先會創(chuàng)建一個兩倍于原容量的數(shù)組,然后將原數(shù)組里的元素進行再hash后插入到新的數(shù)組里。為了高效ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容。

7,ConcurrentHashMap的size操作

  如果我們要統(tǒng)計整個ConcurrentHashMap里元素的大小,就必須統(tǒng)計所有Segment里元素的大小后求和。Segment里的全局變量count是一個volatile變量,那么在多線程場景下,我們是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢?不是的,雖然相加時可以獲取每個Segment的count的最新值,但是拿到之后可能累加前使用的count發(fā)生了變化,那么統(tǒng)計結(jié)果就不準(zhǔn)了。所以最安全的做法,是在統(tǒng)計size的時候把所有Segment的put,remove和clean方法全部鎖住,但是這種做法顯然非常低效。 因為在累加count操作過程中,之前累加過的count發(fā)生變化的幾率非常小,所以ConcurrentHashMap的做法是先嘗試2次通過不鎖住Segment的方式來統(tǒng)計各個Segment大小,如果統(tǒng)計的過程中,容器的count發(fā)生了變化,則再采用加鎖的方式來統(tǒng)計所有Segment的大小。

  那么ConcurrentHashMap是如何判斷在統(tǒng)計的時候容器是否發(fā)生了變化呢?使用modCount變量,在put , remove和clean方法里操作元素前都會將變量modCount進行加1,那么在統(tǒng)計size前后比較modCount是否發(fā)生變化,從而得知容器的大小是否發(fā)生變化。

到此這篇關(guān)于Java ConcurrentHashMap用法案例詳解的文章就介紹到這了,更多相關(guān)Java ConcurrentHashMap講解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring三種方法的注解自動注入問題

    Spring三種方法的注解自動注入問題

    這篇文章主要介紹了Spring三種方法的注解自動注入問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-12-12
  • Java如何使用遞歸查詢多級樹形結(jié)構(gòu)數(shù)據(jù)(多級菜單)

    Java如何使用遞歸查詢多級樹形結(jié)構(gòu)數(shù)據(jù)(多級菜單)

    這篇文章主要介紹了Java如何使用遞歸查詢多級樹形結(jié)構(gòu)數(shù)據(jù)(多級菜單),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • Java異常處理實例詳解

    Java異常處理實例詳解

    這篇文章主要介紹了Java異常處理實例詳解,列舉了實際例子講解的很清晰,有感興趣的同學(xué)可以學(xué)習(xí)下
    2021-03-03
  • 一篇文章帶你了解SpringMVC數(shù)據(jù)綁定

    一篇文章帶你了解SpringMVC數(shù)據(jù)綁定

    這篇文章主要給大家介紹了關(guān)于如何通過一篇文章弄懂Spring MVC的參數(shù)綁定,文中通過示例代碼以及圖文介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-08-08
  • 在Spring Boot中從類路徑加載文件的示例

    在Spring Boot中從類路徑加載文件的示例

    創(chuàng)建Spring Boot Web應(yīng)用程序時,有時有時需要從類路徑中加載文件;war和jar的加載文件格式是不一樣的,在下面,您將找到在WAR和JAR中加載文件的解決方案。
    2020-10-10
  • Java使用多線程異步執(zhí)行批量更新操作方法

    Java使用多線程異步執(zhí)行批量更新操作方法

    這篇文章主要介紹了Java使用多線程異步執(zhí)行批量更新操作,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-01-01
  • Java診斷工具Arthas的快速入門與實踐

    Java診斷工具Arthas的快速入門與實踐

    在Java開發(fā)中,我們經(jīng)常會遇到各種性能問題、內(nèi)存泄漏、線程阻塞等問題,這些問題往往難以通過常規(guī)的日志和監(jiān)控工具來定位和解決,Arthas作為一款開源的Java診斷工具,提供了強大的實時監(jiān)控和診斷功能,本文將詳細(xì)介紹Arthas的安裝、基本使用以及一些常用命令
    2025-02-02
  • java分布式面試接口如何保證冪等及概念理解

    java分布式面試接口如何保證冪等及概念理解

    這篇文章主要為大家介紹了java分布式面試中接口如何保證冪等的問題解答以及概念描述,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步
    2022-03-03
  • Java 回調(diào)函數(shù)深入理解

    Java 回調(diào)函數(shù)深入理解

    這篇文章主要介紹了 Java 回調(diào)函數(shù)深入理解的相關(guān)資料,需要的朋友可以參考下
    2017-03-03
  • Java實現(xiàn)微信支付的簽名算法示例

    Java實現(xiàn)微信支付的簽名算法示例

    這篇文章主要為大家介紹了Java實現(xiàn)微信支付的簽名算法實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-12-12

最新評論

招远市| 交口县| 余干县| 嵩明县| 永靖县| 开原市| 清水县| 普安县| 习水县| 鹿邑县| 龙川县| 武平县| 长寿区| 长岭县| 思南县| 临夏县| 临湘市| 二连浩特市| 哈巴河县| 丘北县| 宁阳县| 长丰县| 绥阳县| 屯门区| 普兰店市| 嘉荫县| 于都县| 白水县| 抚顺县| 老河口市| 奎屯市| 阿克陶县| 兰西县| 炉霍县| 洱源县| 田阳县| 临城县| 平陆县| 英超| 巴林右旗| 丽水市|