HashMap原理詳解

HashMap死鎖

在講解HashMap以前咱們先來看看一段代碼:java

public class HashMapDeadLockTest {
    public static void main(String[] args) { MapResizer map= new MapResizer(); for (int i=0;i<30;i++){ new Thread (new MapResizer()).start(); } } } class MapResizer implements Runnable { public Map<Integer,Integer> map = new HashMap<Integer, Integer>(2); public AtomicInteger atomicInteger = new AtomicInteger(); public void run() { while(atomicInteger.get() < 100000){ map.put(atomicInteger.get(),atomicInteger.get()); atomicInteger.incrementAndGet(); } } }

運行這段代碼,會發現代碼一直處於運行狀態,其實就是發生了死鎖。(運行環境是jdk1.7)。具體怎麼死鎖呢,咱們下面來具體看看:數組

HashMap在擴容的時候會發生死鎖,擴容死鎖的代碼以下:安全

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; //線程E2在此處park //LockSupport.park 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; } } }

假設咱們的線程2運行到下面的狀況的時候Park住:數據結構

 而後咱們的線程1開始運行:根據transfer方法中的代碼,遍歷擴容前的數組,當遍歷到14的時候,發現e!=null,進入循環。e1指向房東,next1 指向卷福,進行rehash之後假設i=20.剛開是newTable[i]=null,因而房東的next指針=null,把房東放在newTable[20]上面,next1賦值給e1,即e1和next1指向卷福,以下圖多線程

 

 而後開始第二輪循環,e=指向卷福不爲空,繼續往下執行,e1.next=null ,則next1指針指向null,rehash之後,假設i依舊=20。newTable[20]=房東,那麼,這時候e1.next指針=房東。(頭部插入),newTable[20]=卷福。最後把next1指針賦值給e1,則e1=next1=null.以下圖:併發

 

 線程1執行完成,線程2被喚醒開始繼續執行。根據上圖能夠看到如今next2指針指向的的卷福即next2=卷福,E2指針指向的是房東,e2=房東。如今繼續第一輪循環,進行rehash,假設rehash之後i仍是20.那麼e.next=newTable[20]=卷福,而後把next2賦值給E2,就是e2=next2=卷福,以下圖:ide

 

 而後繼續開始線程2的第二輪循環,e2=卷福,e.next=房東,那麼next2指向房東,rehash之後i=20.newTable[20]=房東,那麼e.next=房東。而後再把卷福移動到newTable[20],即newTable[20]=卷福。再把next2賦值給E2,就是說next2=e2=房東。以下圖:高併發

 

此時能夠看到卷福和房東的next指針都指向彼此,而後這個擴容就進入了死循環。這就形成咱們的死鎖。這個死鎖只是在jdk1.7會出現,jdk1.8就不會出現了,那麼jdk1.8沒有這個問題是如何避免的,咱們來看一下。測試

咱們先來看一下jdk1.8的resize的代碼:優化

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold  } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } 

根據以上代碼,咱們假設咱們如今在15位置上有一個如下數組:

 

 根據以上的代碼,咱們來進行擴容。

 由於oldTab!=null,進入循環,遍歷老Tab裏面的元素,當j=15的時候咱們的不爲空。此時e指向oldTab[15],進入此條件後將oldTab[15]值爲空,將e移出。以下圖所示:

 

 此時的e.next!=null 則初始化loHead=null,loTail=null,hiHead=null,hiTail=null,next。此時的next指向房東。假設剛開始e.hash&oldCap==0,並且這時候loTail==null,那麼咱們的loHead=e=卷福,loTail=e=卷福,以下圖:

由上圖能夠看到next所指的對象!=null,所以while()循環繼續。第二輪循環的時候將next的值賦值給e,那麼e指針指向房東,next指針指向華生,假設(e.hash & oldCap) == 0那麼此時咱們的loTail!=null,因此咱們要把loTail.next=房東,loTail指向房東,以下圖:

 

 緊接着將next賦值給e,那麼e指向華生,且!=null,那麼咱們繼續while循環。next 指針指向警長,此時假設(e.hash & oldCap) != 0那麼此時咱們就走hiTail,此時hiTail爲空,將e賦值給hiHead,那麼hiHead=華生,hiTail=華生,以下圖:

 

 而後繼續循環,e指向警長,不爲空,next指針指向null,此時的hiTail !=null,那麼hiTail.next=警長,loTail=警長,而後將next賦值給e,此時e=next=null,結束while循環,以下圖:

 

 結束while循環之後繼續往下走loTail!=null ,將loTail.next設置爲null,而後將loHead放置到新table的15位置,即newTab[15],將hiTail.next設置爲空,將hiHead放置到newTab[31]的位置,以下圖:

 

 由以上能夠看出jdk1.8採用的高低位搭配擴容,不會行程死環狀況,這樣就不像jdk1.7兩個指針來回指致使死循環。jdk1.8的hashMap除了數組+鏈表還有紅黑樹結構。

HashMap線程不安全

jdk1.7和jdk1.8在高併發的狀況下都會出現數據丟失和get到null的狀況 。數據丟失就是說多個線程同時put,致使一些值被覆蓋,這樣就形成了數據丟失。get到null值也是同樣的,當多線程put.,get的時候,一個線程尚未put進去就被get了,這個時候就會get到null值。下面的代碼來演示一下數據丟失的狀況。也就是說HashMap在jdk1.7,jdk1.8都是線程不安全的。

 

/**  
* <p>Title: HashMapDataMisingTest.java</p >  
* <p>Description: </p >  
* <p>@datetime 2019年8月18日 上午2:19:31</p >
* <p>$Revision$</p > 
* <p>$Date$</p >
* <p>$Id$</p >
*/  
package com.test;

import java.util.HashMap; import java.util.Map; /** * @author hong_liping * */ public class HashMapDataMisingTest { public static final Map<String ,String> map=new HashMap<String , String>(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { for (int i=0;i<1000;i++){ map.put(String.valueOf(i), String.valueOf(i)); } } }).start(); new Thread(new Runnable() { @Override public void run() { for (int i=1000;i<2000;i++){ map.put(String.valueOf(i), String.valueOf(i)); } } }).start(); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block  e.printStackTrace(); } System.out.println("mapSize:"+map.size()); for (int i=0;i<2000;i++){ System.out.println("value:"+map.get(String.valueOf(i))); } } } //測試結果: mapSize:1995

根據上面的結果咱們能夠看出,咱們的size不對,原本是應該爲2000,可是如今只是1995,有幾筆不見了。爲何會出現上面的狀況呢,就是上述的線程不安全致使的。

  HashMap的基本信息詳解能夠去看一下這篇博客,比較好。https://www.jianshu.com/p/ee0de4c99f87

ConcurrentHashMap線程安全

  根據上述的講解咱們知道在多線程高併發的狀況下hashMap是不安全,在這種狀況下想要使用線程安全的怎麼辦呢,這時候咱們就可使用ConcurrentHashMap。先來看一下jdk7的ConcurrentHashMap的數據結構,以下:

jdk7 ConcurrentHashMap數據結構

 

 

 就是說每一個segment段經過繼承ReetrantLock來進行加鎖,就是每一個段都有個分段鎖,每一個段下面有個table數組,這個table數組就是數組+鏈表的格式.

jdk8 ConcurrentHashMap數據結構

jdk8的時候對這個ConcurrentHashMap的數據結構進行了優化,沒有分段,直接就是Node數組+鏈表+紅黑樹的結構,以下圖所示:

 

 

在JDK8中咱們能夠看到當多個線程併發往同一個空的頭節點插值的時候,可能出現值的覆蓋,jdk8中爲了不這個問題使用的CAS操做,這樣就避免了值丟失的問題。同時還有一個參數須要去關注,SIZECTL,初始值爲SIZECTL=-1,當SIZECTL=-1的時候表示正在擴容,尚未擴容完成,當SIZECTL>0表示擴容已經完成。

爲何說是線程安全的呢,咱們來看一下:

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin  } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }

從上面的代碼也能夠看出ConcurrentHashMap在進行put操做的時候使用了Synchronized關鍵字,這樣也保證了線程安全。

以上有疑問歡迎各位小夥伴們來一塊兒討論。

相關文章
相關標籤/搜索