HashMap 死循定位以及JDK8對它的優化

HashMap 是非線程安全的,在多線程處理場景下,嚴禁使用。多線程要用ConcurrentHashMap。

大家都知道,相比於HashTable,HashMap是一個非線程安全的實現類。

爲什麼說HashMap是非線程安全的呢?因爲在高併發情況下,HashMap在一些操作上會存在問題,如死循環問題,導致CPU使用率較高。

下面來看下怎麼復現這個問題。如下代碼所示,我們創建10個線程,這10個線程併發向一個HashMap種添加元素。

package com.light.sword

import java.util.*
import java.util.concurrent.atomic.AtomicInteger


/**
 * @author: Jack
 * 2020-02-21 13:52
 */

fun main() {
    val map = HashMap<Int, Int>()
    val atomicInt = AtomicInteger(0)

    for (i in 0..9) {
        Thread {
            while (atomicInt.get() < 1000000) {
                map[atomicInt.get()] = atomicInt.get()
                atomicInt.incrementAndGet()
            }
        }.start()
    }
}

我們運行main方法後,發現代碼一直卡死並沒有退出。CPU 飆到了 892% 。

接下來我們 jpsjstack 命令看下這個進程的狀態。

$ jps
20642 Launcher
28630 Jps
23430 Launcher
6233 
28618 KotlinCompileDaemon
28621 Launcher
28622 HashMapLockDemoKt
$ jstack 28622
2020-02-21 18:36:50
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.40-b25 mixed mode):

"Attach Listener" #22 daemon prio=9 os_prio=31 tid=0x00007fdfda8c2000 nid=0x9b07 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #21 prio=5 os_prio=31 tid=0x00007fdfdf044800 nid=0x2603 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-8" #19 prio=5 os_prio=31 tid=0x00007fdfda891000 nid=0x9d03 runnable [0x000070000c461000]
   java.lang.Thread.State: RUNNABLE
    at java.util.HashMap$TreeNode.root(HashMap.java:1808)
    at java.util.HashMap$TreeNode.putTreeVal(HashMap.java:1963)
    at java.util.HashMap.putVal(HashMap.java:637)
    at java.util.HashMap.put(HashMap.java:611)
    at com.light.sword.HashMapLockDemoKt$main$1.run(HashMapLockDemo.kt:19)
    at java.lang.Thread.run(Thread.java:745)

"Thread-7" #18 prio=5 os_prio=31 tid=0x00007fdfda890000 nid=0x5d03 runnable [0x000070000c35e000]
   java.lang.Thread.State: RUNNABLE
    at java.util.HashMap$TreeNode.root(HashMap.java:1808)
    at java.util.HashMap$TreeNode.putTreeVal(HashMap.java:1963)
    at java.util.HashMap.putVal(HashMap.java:637)
    at java.util.HashMap.put(HashMap.java:611)
    at com.light.sword.HashMapLockDemoKt$main$1.run(HashMapLockDemo.kt:19)
    at java.lang.Thread.run(Thread.java:745)
。。。

"Service Thread" #10 daemon prio=9 os_prio=31 tid=0x00007fdfdd831800 nid=0x5603 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #9 daemon prio=9 os_prio=31 tid=0x00007fdfda875800 nid=0x5503 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

看代碼堆棧:

    at java.util.HashMap$TreeNode.root(HashMap.java:1808)
    at java.util.HashMap$TreeNode.putTreeVal(HashMap.java:1963)
    at java.util.HashMap.putVal(HashMap.java:637)
    at java.util.HashMap.put(HashMap.java:611)
    at com.light.sword.HashMapLockDemoKt$main$1.run(HashMapLockDemo.kt:19)
    at java.lang.Thread.run(Thread.java:745)

從上面看到,在 HashMapTreeNode.root() 方法的第 1808 行出現了問題。堆棧入口是 HashMap.put(HashMap.java:611)

HashMap 數據結構

Java8中對HashMap進行了優化,如果鏈表中元素超過8個時,就將鏈表轉化爲紅黑樹,以減少查詢的複雜度,將時間複雜度降低爲O(logN)。

HashMap沒有對多線程的場景下做任何的處理,不用說別的,就兩個線程同時put,然後衝突了,兩者需要操作一個鏈表/紅黑樹,這肯定就會有錯誤發生,所以HashMap是線程不安全的。

Node 和 TreeNode

Java 8 中使用 Node 模型來代表每個 HashMap 中的數據節點,都是 key,value,hash 和 next 這四個屬性。Node 用於鏈表,紅黑樹用 TreeNode。

HashMap 中使用 Node[] table 數組來存儲元素。可以看出,HashMap的底層還是數組(數組會在 put 的時候通過 resize() 函數進行分配),數組的長度爲2的N次冪。

TreeNode 中封裝了對紅黑樹的基本操作:

HashMap、Hashtable、ConccurentHashMap 三者的區別

HashMap線程不安全,數組+鏈表+紅黑樹 Hashtable線程安全,鎖住整個對象,數組+鏈表 ConccurentHashMap 線程安全,CAS+同步鎖、數組+鏈表+紅黑樹 HashMap的key,value均可爲null,其他兩個不行。

ConccurentHashMap 在 JDK1.7 和 JDK1.8 中的區別:

這是Java7中實現線程安全的思路,ConcurrentHashMap由16個segment組成,每個segment就相當於一個HashMap(數組+鏈表)。

segment最多16個,想要擴容,就是擴充每個segment中數組的長度。

然後只要實現每個segment是線程安全的,就讓這個Map線程安全了。每個segment是加鎖的,對修改segment的操作加鎖,不影響其他segment的使用,所以理想情況下,最多支持16個線程併發修改segment,這16個線程分別訪問不同的segment。

同時,在segment加鎖時,所有讀線程是不會受到阻塞的。

這樣設計,put與get的基本操作就是先找segment,再找segment中的數組位置,再查鏈表。

在JDK1.8主要設計上的改進有以下幾點:

1、不採用segment而採用node,鎖住node來實現減小鎖粒度。 2、設計了MOVED狀態 當resize的中過程中 線程2還在put數據,線程2會幫助resize。 3、使用3個CAS操作來確保node的一些操作的原子性,這種方式代替了鎖。 4、sizeCtl的不同值來代表不同含義,起到了控制的作用。 採用synchronized而不是ReentrantLock

java.lang.Thread.State 類

public static enum Thread.Stateextends Enum<Thread.State>線程狀態。線程可以處於下列狀態之一:

1.NEW 至今尚未啓動的線程的狀態。

2.RUNNABLE 可運行線程的線程狀態。處於可運行狀態的某一線程正在 Java 虛擬機中運行,但它可能正在等待操作系統中的其他資源,比如處理器。

3.BLOCKED 受阻塞並且正在等待監視器鎖的某一線程的線程狀態。處於受阻塞狀態的某一線程正在等待監視器鎖,以便進入一個同步的塊/方法,或者在調用 Object.wait 之後再次進入同步的塊/方法。

4.WAITING 某一等待線程的線程狀態。某一線程因爲調用下列方法之一而處於等待狀態:

不帶超時值的 Object.wait 不帶超時值的 Thread.join

LockSupport.park 處於等待狀態的線程正等待另一個線程,以執行特定操作。 例如,已經在某一對象上調用了 Object.wait() 的線程正等待另一個線程,以便在該對象上調用 Object.notify() 或 Object.notifyAll()。已經調用了 Thread.join() 的線程正在等待指定線程終止。

5.TIMED_WAITING具有指定等待時間的某一等待線程的線程狀態。某一線程因爲調用以下帶有指定正等待時間的方法之一而處於定時等待狀態:

Thread.sleep 帶有超時值的 Object.wait 帶有超時值的 Thread.join LockSupport.parkNanos LockSupport.parkUntil

6.TERMINATED 已終止線程的線程狀態。線程已經結束執行。

注意:在給定時間點上,一個線程只能處於一種狀態。這些狀態是虛擬機狀態,它們並沒有反映所有操作系統線程狀態。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章