日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當(dāng)前位置:首頁(yè) > > 架構(gòu)師社區(qū)
[導(dǎo)讀]我們都知道,HashMap在并發(fā)環(huán)境下使用可能出現(xiàn)問(wèn)題,但是具體表現(xiàn),以及為什么出現(xiàn)并發(fā)問(wèn)題,可能并不是所有人都了解 這篇文章記錄一下HashMap在多線程環(huán)境下可能出現(xiàn)的問(wèn)題以及如何避免。 在分析HashMap的并發(fā)問(wèn)題前,先簡(jiǎn)單了解HashMap的put和get基本操作是


我們都知道,HashMap在并發(fā)環(huán)境下使用可能出現(xiàn)問(wèn)題,但是具體表現(xiàn),以及為什么出現(xiàn)并發(fā)問(wèn)題,可能并不是所有人都了解

這篇文章記錄一下HashMap在多線程環(huán)境下可能出現(xiàn)的問(wèn)題以及如何避免。

在分析HashMap的并發(fā)問(wèn)題前,先簡(jiǎn)單了解HashMap的put和get基本操作是如何實(shí)現(xiàn)的。

1.HashMap的put和get操作

大家知道HashMap內(nèi)部實(shí)現(xiàn)是通過(guò)拉鏈法解決哈希沖突的,也就是通過(guò)鏈表的結(jié)構(gòu)保存散列到同一數(shù)組位置的兩個(gè)值,

put操作主要是判空,對(duì)key的hashcode執(zhí)行一次HashMap自己的哈希函數(shù),得到bucketindex位置,還有對(duì)重復(fù)key的覆蓋操作。

對(duì)照源碼分析一下具體的put操作是如何完成的:

public V put(K key, V value{
        if (key == null)
            return putForNullKey(value);
        //得到key的hashcode,同時(shí)再做一次hash操作
        int hash = hash(key.hashCode());
        //對(duì)數(shù)組長(zhǎng)度取余,決定下標(biāo)位置
        int i = indexFor(hash, table.length);
        /**
          * 首先找到數(shù)組下標(biāo)處的鏈表結(jié)點(diǎn),
          * 判斷key對(duì)一個(gè)的hash值是否已經(jīng)存在,如果存在將其替換為新的value
          */

        for (Entry  e = table[i]; e !=  null; e = e.next) {
            Object k;
             if (e.hash == hash && ((k = e.key) == key || key. equals(k))) {
                V oldValue = e. value;
                e. value =  value;
                e.recordAccess( this);
                 return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key,  value, i);
         return  null;
    }

涉及到的幾個(gè)方法:

static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

static int indexFor(int h, int length) {
        return h & (length-1);
    }

數(shù)據(jù)put完成以后,就是如何get,我們看一下get函數(shù)中的操作:

public V get(Object key{
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        /**
          * 先定位到數(shù)組元素,再遍歷該元素處的鏈表
          * 判斷的條件是key的hash值相同,并且鏈表的存儲(chǔ)的key值和傳入的key值相同
          */

        for (Entry  e = table[indexFor(hash, table.length)];e !=  null;e = e.next) {
            Object k;
             if (e.hash == hash && ((k = e.key) == key || key. equals(k)))
                 return e. value;
        }
         return  null;
}

看一下鏈表的結(jié)點(diǎn)數(shù)據(jù)結(jié)構(gòu),保存了四個(gè)字段,包括key,value,key對(duì)應(yīng)的hash值以及鏈表的下一個(gè)節(jié)點(diǎn):

static class Entry<K,Vimplements Map.Entry<K,V{
       final K key;//Key-value結(jié)構(gòu)的key
       V value;//存儲(chǔ)值
       Entry  next; //指向下一個(gè)鏈表節(jié)點(diǎn)
        final  int hash; //哈希值
 }

2.Rehash/再散列擴(kuò)展內(nèi)部數(shù)組長(zhǎng)度

哈希表結(jié)構(gòu)是結(jié)合了數(shù)組和鏈表的優(yōu)點(diǎn),在最好情況下,查找和插入都維持了一個(gè)較小的時(shí)間復(fù)雜度O(1)

不過(guò)結(jié)合HashMap的實(shí)現(xiàn),考慮下面的情況,如果內(nèi)部Entry[] tablet的容量很小,或者直接極端化為table長(zhǎng)度為1的場(chǎng)景,那么全部的數(shù)據(jù)元素都會(huì)產(chǎn)生碰撞

這時(shí)候的哈希表成為一條單鏈表,查找和添加的時(shí)間復(fù)雜度變?yōu)镺(N),失去了哈希表的意義。

所以哈希表的操作中,內(nèi)部數(shù)組的大小非常重要,必須保持一個(gè)平衡的數(shù)字,使得哈希碰撞不會(huì)太頻繁,同時(shí)占用空間不會(huì)過(guò)大。

這就需要在哈希表使用的過(guò)程中不斷的對(duì)table容量進(jìn)行調(diào)整,看一下put操作中的addEntry()方法:

void addEntry(int hash, K key, V valueint bucketIndex{
   Entry  e = table[bucketIndex];
       table[bucketIndex] =  new Entry (hash, key,  value, e);
        if (size++ >= threshold)
           resize( 2 * table.length);
   }

這里面resize的過(guò)程,就是再散列調(diào)整table大小的過(guò)程,默認(rèn)是當(dāng)前table容量的兩倍。

void resize(int newCapacity) {
       Entry[] oldTable = table;
       int oldCapacity = oldTable.length;
       if (oldCapacity == MAXIMUM_CAPACITY) {
           threshold = Integer.MAX_VALUE;
           return;
       }

       Entry[] newTable = new Entry[newCapacity];
       //初始化一個(gè)大小為oldTable容量?jī)杀兜男聰?shù)組newTable
       transfer(newTable);
       table = newTable;
       threshold = (int)(newCapacity * loadFactor);
   }

關(guān)鍵的一步操作是transfer(newTable),這個(gè)操作會(huì)把當(dāng)前Entry[] table數(shù)組的全部元素轉(zhuǎn)移到新的table中,這個(gè)transfer的過(guò)程在并發(fā)環(huán)境下會(huì)發(fā)生錯(cuò)誤,導(dǎo)致數(shù)組鏈表中的鏈表形成循環(huán)鏈表,在后面的get操作時(shí)e = e.next操作無(wú)限循環(huán),Infinite Loop出現(xiàn)。

下面具體分析HashMap的并發(fā)問(wèn)題的表現(xiàn)以及如何出現(xiàn)的。

3.HashMap在多線程put后可能導(dǎo)致get無(wú)限循環(huán)

HashMap在并發(fā)環(huán)境下多線程put后可能導(dǎo)致get死循環(huán),具體表現(xiàn)為CPU使用率100%,看一下transfer的過(guò)程:

void transfer(Entry[] newTable{
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry  e = src[j];
             if (e !=  null) {
                src[j] =  null;
                 do {
         //假設(shè)第一個(gè)線程執(zhí)行到這里因?yàn)槟撤N原因掛起
                    Entry  next = e.next;
                     int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }  while (e !=  null);
            }
        }
    }

這里引用酷殼陳皓的博文:

http://coolshell.cn/articles/9606.html

并發(fā)下的Rehash

1)假設(shè)我們有兩個(gè)線程。我用紅色和淺藍(lán)色標(biāo)注了一下。

我們?cè)倩仡^看一下我們的 transfer代碼中的這個(gè)細(xì)節(jié):

do {
    Entry   next = e. next; // <--假設(shè)線程一執(zhí)行到這里就被調(diào)度掛起了
    int i = indexFor(e.hash, newCapacity);
    e. next = newTable[i];
    newTable[i] = e;
    e =  next;
while (e != null);

而我們的線程二執(zhí)行完成了。于是我們有下面的這個(gè)樣子。

HashMap 在并發(fā)下可能出現(xiàn)的問(wèn)題分析!

注意,因?yàn)門hread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。我們可以看到鏈表的順序被反轉(zhuǎn)后。

2)線程一被調(diào)度回來(lái)執(zhí)行。

  • 先是執(zhí)行 newTalbe[i] = e;

  • 然后是e = next,導(dǎo)致了e指向了key(7)

  • 而下一次循環(huán)的next = e.next導(dǎo)致了next指向了key(3)

HashMap 在并發(fā)下可能出現(xiàn)的問(wèn)題分析!

3)一切安好。

線程一接著工作。把key(7)摘下來(lái),放到newTable[i]的第一個(gè),然后把e和next往下移。

HashMap 在并發(fā)下可能出現(xiàn)的問(wèn)題分析!

4)環(huán)形鏈接出現(xiàn)。

e.next = newTable[i] 導(dǎo)致 key(3).next 指向了 key(7)

注意:此時(shí)的key(7).next 已經(jīng)指向了key(3), 環(huán)形鏈表就這樣出現(xiàn)了。

HashMap 在并發(fā)下可能出現(xiàn)的問(wèn)題分析!


于是,當(dāng)我們的線程一調(diào)用到,HashTable.get(11)時(shí),悲劇就出現(xiàn)了——Infinite Loop。

針對(duì)上面的分析模擬這個(gè)例子,

這里在run中執(zhí)行了一個(gè)自增操作,i++非原子操作,使用AtomicInteger避免可能出現(xiàn)的問(wèn)題:

public class MapThread extends Thread{
        /**
         * 類的靜態(tài)變量是各個(gè)實(shí)例共享的,因此并發(fā)的執(zhí)行此線程一直在操作這兩個(gè)變量
         * 選擇AtomicInteger避免可能的int++并發(fā)問(wèn)題
         */

         private static AtomicInteger ai = new AtomicInteger(0);
         //初始化一個(gè)table長(zhǎng)度為1的哈希表
         private static HashMap  map =  new HashMap ( 1);
          //如果使用ConcurrentHashMap,不會(huì)出現(xiàn)類似的問(wèn)題
//       private static ConcurrentHashMap  map = new ConcurrentHashMap (1);

          public void run()
          
{
               while (ai. get() <  100000)
              {   //不斷自增
                  map.put(ai. get(), ai. get());
                  ai.incrementAndGet();
               }

              System. out.println(Thread.currentThread().getName()+ "線程即將結(jié)束");
          }
    }

測(cè)試一下:

public static void main(String[] args){
         MapThread t0 = new MapThread();
         MapThread t1 = new MapThread();
         MapThread t2 = new MapThread();
         MapThread t3 = new MapThread();
         MapThread t4 = new MapThread();
         MapThread t5 = new MapThread();
         MapThread t6 = new MapThread();
         MapThread t7 = new MapThread();
         MapThread t8 = new MapThread();
         MapThread t9 = new MapThread();

         t0.start();
         t1.start();
         t2.start();
         t3.start();
         t4.start();
         t5.start();
         t6.start();
         t7.start();
         t8.start();
         t9.start();

    }

注意并發(fā)問(wèn)題并不是一定會(huì)產(chǎn)生,可以多執(zhí)行幾次,我試驗(yàn)了上面的代碼很容易產(chǎn)生無(wú)限循環(huán),控制臺(tái)不能終止,有線程始終在執(zhí)行中,這是其中一個(gè)死循環(huán)的控制臺(tái)截圖

可以看到六個(gè)線程順利完成了put工作后銷毀,還有四個(gè)線程沒有輸出,卡在了put階段,感興趣的可以斷點(diǎn)進(jìn)去看一下:

HashMap 在并發(fā)下可能出現(xiàn)的問(wèn)題分析!

上面的代碼,如果把注釋打開,換用ConcurrentHashMap就不會(huì)出現(xiàn)類似的問(wèn)題。

4.多線程put的時(shí)候可能導(dǎo)致元素丟失

HashMap另外一個(gè)并發(fā)可能出現(xiàn)的問(wèn)題是,可能產(chǎn)生元素丟失的現(xiàn)象。

考慮在多線程下put操作時(shí),執(zhí)行addEntry(hash, key, value, i),如果有產(chǎn)生哈希碰撞,導(dǎo)致兩個(gè)線程得到同樣的bucketIndex去存儲(chǔ),就可能會(huì)出現(xiàn)覆蓋丟失的情況:

void addEntry(int hash, K key, V valueint bucketIndex{
    //多個(gè)線程操作數(shù)組的同一個(gè)位置
    Entry  e = table[bucketIndex];
        table[bucketIndex] =  new Entry (hash, key,  value, e);
         if (size++ >= threshold)
            resize( 2 * table.length);
    }

5.使用線程安全的哈希表容器

那么如何使用線程安全的哈希表結(jié)構(gòu)呢,這里列出了幾條建議:

  • 使用Hashtable 類,Hashtable 是線程安全的;

  • 使用并發(fā)包下的java.util.concurrent.ConcurrentHashMap,ConcurrentHashMap實(shí)現(xiàn)了更高級(jí)的線程安全;

  • 或者使用synchronizedMap() 同步方法包裝 HashMap object,得到線程安全的Map,并在此Map上進(jìn)行操作。


參考

http://coolshell.cn/articles/9606.html


作者:邴越

https://www.cnblogs.com/binyue/p/3726403.html

特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:

HashMap 在并發(fā)下可能出現(xiàn)的問(wèn)題分析!

HashMap 在并發(fā)下可能出現(xiàn)的問(wèn)題分析!

HashMap 在并發(fā)下可能出現(xiàn)的問(wèn)題分析!

長(zhǎng)按訂閱更多精彩▼

HashMap 在并發(fā)下可能出現(xiàn)的問(wèn)題分析!

如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除( 郵箱:macysun@21ic.com )。
關(guān)閉