自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

多線程下HashMap是怎么死循環(huán)的?

開發(fā) 前端
競態(tài)條件可能會導(dǎo)致程序崩潰、數(shù)據(jù)損壞、死鎖等問題。為了避免競爭條件,可以使用同步機制(如鎖、信號量等)來保證只有一個進程或線程可以訪問共享資源。

這是一個老問題了,現(xiàn)在都Java21了,又翻出來Java7的問題,算是對歷史的總結(jié)把。

背過面試八股文的都知道,HashMap是非線程安全的,多線程下要用ConcurrentHashMap之類的。但是實際工作中,還是會碰到在多線程中使用HashMap??赡苁菍懙臅r候迷糊了,也可能是代碼升級時沒有注意,比如原本是單線程的,后來性能不行改成多線程了。多線程下使用HashMap,偶爾會出現(xiàn)服務(wù)hang死的情況,重啟就好,測試環(huán)境還復(fù)現(xiàn)不了,純偶發(fā),即Race Condition(競態(tài)條件)。

競態(tài)條件(Race Condition)是指在多線程或者多進程的程序中,由于多個線程或進程之間執(zhí)行順序的不確定性,導(dǎo)致程序出現(xiàn)意料之外的結(jié)果或者行為。這種情況通常發(fā)生在多個線程或進程同時訪問共享資源時,其中一個線程或進程修改了共享資源的狀態(tài),但其他線程或進程并沒有意識到這個修改,導(dǎo)致它們基于過期的狀態(tài)做出了錯誤的決策。競爭條件是一種常見的并發(fā)編程錯誤,需要在程序設(shè)計和實現(xiàn)時特別注意。

競態(tài)條件可能會導(dǎo)致程序崩潰、數(shù)據(jù)損壞、死鎖等問題。為了避免競爭條件,可以使用同步機制(如鎖、信號量等)來保證只有一個進程或線程可以訪問共享資源。

那我們來分析下,HashMap是怎么形成競態(tài)條件的。

先梳理一下源碼

put方法是入口:

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    // 計算hash值
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    // 如果該key已被插入,則替換舊的value
    for (Entry<K,V> 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++;
    // 如果該key不存在,增加一個節(jié)點
    addEntry(hash, key, value, i);
    return null;
}

檢查容量是否超過閾值,超過了就擴容:

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 判斷當(dāng)前的size是否超過閾值,如果超過了,重新resize,也就是擴容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    // size未超過閾值或者擴容完成后,增加節(jié)點
    createEntry(hash, key, value, bucketIndex);
}

將新增元素插入鏈表中:

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

擴容重建新的hash表,并將老表中的數(shù)據(jù)遷移到新表中:

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

    // 創(chuàng)建新的hash表
    Entry[] newTable = new Entry[newCapacity];
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    // 將老表中的數(shù)據(jù)遷移到新表中
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

老表數(shù)據(jù)遷移到新表,就是循環(huán)遍歷舊數(shù)據(jù),然后插入的新表中:

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

上面的這個代碼沒有什么問題,有問題也不會堅持這么多版本了。

那問題出在哪呢?出在了多線程上,接下來我們畫圖看看。

看圖說話

本節(jié)中的例子和圖片思路來源于酷殼。

假設(shè)我們的hash算法就是key mode表格大小,hash表size是2,當(dāng)前元素key是3、5、7,mod 2時hash值都是1。執(zhí)行resize成4時,所有元素遷移到新表數(shù)據(jù),過程如下(下圖來源于酷殼):

示例示例

上面的過程是正常運行的結(jié)果,如果在遷移時有兩個現(xiàn)成同時操作(能過到同時遷移,說明前置的都是同時執(zhí)行的),就會出現(xiàn)問題,還是通過示意圖的方式:

怎么解決呢?

想要成功的解決競態(tài)條件問題,保證程序可以正確的按邏輯順序運行,從理論上應(yīng)該滿足以下四個條件:

  • 不會有兩個及以上進程同時出現(xiàn)在他們的critical section;
  • 不要做任何關(guān)于CPU速度和數(shù)量的假設(shè);
  • 任何進程在運行到critical section之外時都不能阻塞其他進程;
  • 不會有進程永遠等在critical section之前。

"Critical section" 是計算機科學(xué)中的一個概念,用于描述多個進程或線程訪問共享資源時的同步問題。在一個程序中,當(dāng)多個進程或線程需要同時訪問共享資源(如共享內(nèi)存或文件),為了避免數(shù)據(jù)競爭和其他同步問題,需要將訪問共享資源的代碼段限制在一個受保護的區(qū)域內(nèi),這個區(qū)域就稱為 "critical section"。在這個區(qū)域內(nèi),只有一個進程或線程可以訪問共享資源,其他進程或線程必須等待,直到進程或線程退出該區(qū)域。這樣可以確保共享資源的一致性和正確性。

為了避免這種情況的發(fā)生,我們可以采用以下幾種方法:

  1. 使用ConcurrentHashMap:ConcurrentHashMap是Java提供的一種線程安全的哈希表實現(xiàn)。它采用了分段鎖的機制,在多線程環(huán)境下能夠保證高效并發(fā)訪問。如果我們需要在多線程環(huán)境下使用哈希表,建議使用ConcurrentHashMap來代替HashMap。
  2. 使用Collections.synchronizedMap:Collections.synchronizedMap是Java提供的一種線程安全的Map實現(xiàn)。它通過對Map的所有方法進行同步,來保證線程安全。但是,由于它采用了同步的機制,因此在高并發(fā)環(huán)境下可能會出現(xiàn)性能瓶頸。
  3. 使用Lock:我們也可以使用Lock來手動控制對HashMap的訪問。具體來說,我們可以在對HashMap進行操作時,先獲取一個鎖,然后再進行操作。這種方式需要我們手動控制鎖的釋放,因此比較容易出現(xiàn)死鎖的情況。


責(zé)任編輯:武曉燕 來源: 看山的小屋
相關(guān)推薦

2013-06-06 13:34:56

HashMap線程不安全

2020-12-17 07:39:30

HashMap死循環(huán)數(shù)據(jù)

2023-01-31 08:24:55

HashMap死循環(huán)

2022-01-24 07:01:20

安全多線程版本

2022-01-20 08:44:25

HashMap死循環(huán)開放性

2022-01-18 06:59:50

HashMap循環(huán)底層

2020-09-29 15:24:07

面試數(shù)據(jù)結(jié)構(gòu)Hashmap

2013-06-06 13:10:44

HashMap無鎖

2020-05-27 12:45:52

HashMapJava加載因子

2010-03-17 19:24:38

Java多線程循環(huán)

2021-06-11 11:28:22

多線程fork單線程

2018-10-10 20:20:14

2011-06-22 16:08:40

Qt 多線程 事件循環(huán)

2024-10-16 09:34:50

2020-11-13 07:16:09

線程互斥鎖死循環(huán)

2024-03-22 12:29:03

HashMap線程

2023-10-19 08:30:58

線程源碼thread

2011-10-31 15:59:56

SQLiteiPhoneiOS

2011-09-07 10:13:04

IPv6IPv4

2024-12-06 16:00:00

C++頭文件
點贊
收藏

51CTO技術(shù)棧公眾號