hashmap從入門到死鎖,再到分段式鎖

本文主要內容如下:在jdk1.7和jdk1.8下

hashmap put和get的原理,和可能造成的問題

concurrentHashMap的原理

hashmap入門

直接NEW出來就可以了,想要獲取詳細信息的這個直接看源碼,源碼上說的更仔細;

主要有兩個參數,一個是負載因子(需要擴容的比例),一個是初始化的大小。

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

綜上所述,入門完畢;

JDK1.7 HashMap的put源碼分析

hashmap的put操作的主要步驟:

  1. 判斷KEY是否爲NULL,如果爲NULL,就將VALUE放進去
  2. 求出KEY的hash值
  3. 由hash值求出對應的數組位置
  4. 判斷此key是否已經存在了,如果存在了就將value進行替換並返回舊值
  5. 添加新的值到hashmap中

也就是源碼中的如下代碼:

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        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++;
        addEntry(hash, key, value, i);
        return null;
    }

其中相對複雜一點的代碼主要在addEntry這個方法中,此方法會去判斷是否需要去擴容,如果需要去擴容的話,則需要將hashmap的entry數組的值進行一次轉移,由舊的entry數組轉移到新的entry數組中,其代碼爲:

    /**
     * Transfers all entries from current table to newTable.
     */
    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;
            }
        }
    }

JDK1.7中HashMap的put方法的主要過程便是上面這樣的一個大致過程,整體上來說還算是比較簡單的。

JDK1.7 HashMap死鎖

 hashmap死鎖的死鎖問題不是我說的,是網上大家都這麼說的,不信的話可以百度一把。在沒有看源碼前時的我也信以爲真了,以爲hashmap裏的put方法真的會發生死鎖,但是在看了源碼後我感覺被騙了,put方法在多線程情況下確實會有問題。爲此我也特意寫了一段代碼來驗證,代碼如下:

import java.util.HashMap;
import java.util.UUID;

public class HashMapStu {
    public static void main(String[] args) throws Exception {
        final HashMap<String, String> map = new HashMap<String, String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    map.put(UUID.randomUUID().toString(), "");
                }
            }, "mythread-" + i).start();
        }

        System.out.println("ok!");
    }


}

然後運行幾次,看看會不會出現什麼狀況;

按照上面的代碼可以看出,正常情況下當控制檯輸出了"ok!"後程序就會停止的,但是我在JDK1.7的環境下運行了幾次發現並沒有停止運行,並且電腦中的此程序的CPU佔有量很大

然後用jstack打印出此進程的線程信息發現有一個線程一直處於運行狀態:

Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode):

"DestroyJavaVM" prio=5 tid=0x00007fdef60a2800 nid=0x2803 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"mythread-8880" prio=5 tid=0x00007fdef5048800 nid=0x3c27 runnable [0x000070000f1ed000]
   java.lang.Thread.State: RUNNABLE
	at java.util.HashMap.put(HashMap.java:494)
	at HashMapStu$1.run(HashMapStu.java:11)
	at java.lang.Thread.run(Thread.java:745)

"Attach Listener" daemon prio=5 tid=0x00007fdef5847000 nid=0x8e23 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Service Thread" daemon prio=5 tid=0x00007fdef684a000 nid=0x3603 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" daemon prio=5 tid=0x00007fdef5810800 nid=0x3503 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" daemon prio=5 tid=0x00007fdef580c800 nid=0x4603 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=5 tid=0x00007fdef5023000 nid=0x4807 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=5 tid=0x00007fdef6837800 nid=0x2e03 in Object.wait() [0x000070000e952000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000007c000a210> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
	- locked <0x00000007c000a210> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" daemon prio=5 tid=0x00007fdef6000000 nid=0x5103 in Object.wait() [0x000070000e84f000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000007c0008560> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:503)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
	- locked <0x00000007c0008560> (a java.lang.ref.Reference$Lock)

"VM Thread" prio=5 tid=0x00007fdef5018800 nid=0x2c03 runnable 

"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007fdef500e000 nid=0x2207 runnable 

"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007fdef500f000 nid=0x1f03 runnable 

"GC task thread#2 (ParallelGC)" prio=5 tid=0x00007fdef6814000 nid=0x2a03 runnable 

"GC task thread#3 (ParallelGC)" prio=5 tid=0x00007fdef6814800 nid=0x5403 runnable 

"VM Periodic Task Thread" prio=5 tid=0x00007fdef5020800 nid=0x4303 waiting on condition 

JNI global references: 142

提示第494行一直在運行,在源碼中找到也就是這一行了:

for (Entry<K,V> e = table[i]; e != null; e = e.next) {

這一行是一個for循環,想要讓這一行一直運行讓e一直都不等於null即可,

而e又等於e.next,e是一個鏈表。想讓e一直不爲null,讓e這個鏈表中有環就可以了。

那麼現在程序一直運行在494行,那麼也就是e是一個有環的鏈表了,那麼是哪裏造成了這樣的情況的呢?

答案是在多線程情況下對hashmap進行擴容時造成的,也就是上面貼出來的transfer的代碼。具體原因可以考慮下面的場景:

    /**
     * Transfers all entries from current table to newTable.
     */
    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;
            }
        }
    }

假設某一個entry節點的值爲:1>2>3>null

有兩個線程分別爲:P1、P2

按照時間先後順序有時間T1、T2、T3、T4、T5、T6

T1時間點:P1線程運行完Entry<K,V> next = e.next這行後,其值爲:

e=1>2>3>null

next=2>3>null

newTable[i]=1>2>3>null

然後P1線程就wait住了

T2時間點:P2線程將整個方法運行完畢了,擴容已經完成了,也就是while方法已經執行完畢了,其值爲:

e=null

next=null

newTable[i]=3>2>1>null

T3時間點:P1線程獲得CPU的執行權了,P1線程繼續執行,當P1線程執行一次while循環的代碼後,其值爲:

e=2>1>null

next=2>3>null

newTable[i]=1>3>2>1>null

 

那麼newTable[i]這個節點就造成循環鏈表了!然後再去執行鏈表的循環時就會一直執行下去

 

JDK1.7 concurrentHashMap的put方法

 

空了來寫

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