java的Hashmap和ConcurrentHashMap底層原理

一、Hashmap

      1) jdk7,使用數據結構,數組(Entry<K,V>[]  table)+鏈表,源碼如下:

       ① 當hashmap沒有初始化時,初始化hash,hashmap由Entry<K,V>[]  table組成,初始化時初始值大小爲2的冪次方大小,比如 HashMap hashMap = new HashMap(17,0.75f);想要初始化長度爲17,最終會被轉化成32(爲了後面位運算,方便通過hashcode進行位運算時,獲取到key對應的在數組中的位置),根據擴容擴容因子計算一下擴容的閾值。

       ② hashmap是允許key和value爲null的,只不過會默認放置到table數組的第0個位置。

       ③ 根據key的hashcode計算應該存放到table[]數組的哪個位置下。

               1> 下圖對於hashcode的計算主要是爲了降低hashcode的重複率,先降碰撞率。

              2>這裏的&操作就是爲了計算生成的hashcode在table[]數據的哪個位置,相當與取模運算,length-1,因爲length爲2的冪次方比如16,用二級製表示爲: ... 0001 0000,那麼16-1=15,二級製表示: ... 0000 1111,&運算時只有最後四位生效,其餘 全是0,並且最後四位的值就是h的值,相當於 h 對 15 取餘運算(只對2的冪次方有效,所以會默認生成2的冪次方大小的長度)。

        ③ 判斷,如果對應的table[]數組位置不爲空,則遍歷數組下的鏈表,直到找到爲空或者找到一個完全相同的key。key相同則更新key對應value,如果找不到對應的key,則跳出循環,對hashmap的modCount+1,記錄hashmap被修改的次數(快速失敗的時候會用到),進行第四步。

         ④ 核心步驟,添加元素,這裏也分兩步,擴容和添加數據。

     1) 擴容,擴容條件:大於閾值,默認16*0.75=12,存放數據時,對應的數組位置是否爲空,擴容爲原來的2倍。

          1> new 一個新的tables[]數組,然後進行數據轉移。        

            2> 數據轉移,其實就是把原來數據重新進行計算下hashcode,然後放入到新的table[]數組中結束。

           

     2) 存數據,其實就是新生成了一個entry,然後取出table[]數組中原來的頭結點,放入新的entry中,再放入的到對應的table[]數組中(頭插法),並給size +1結束。

     

⑤ Hashmap對比HashTable:

      1)HashTable存儲的value不允許爲null值。

      2)  HashTable 是線程安全的,因爲hashTable爲了保證線程安全,把對應的get和put等方法都加上了synchronized關鍵字來確保線程安全。

     3)  HashTable執行效率不高。

2) jdk8,數據結構: 數組+鏈表+紅黑樹。

      ①  如果未初始化,則初始化hashmap,resize()方法也可以進行初始化操作,取出key對應的hashcode進行&運算(與jdk7是一樣的思路),計算出對應數組的位置,如果爲空這新建一個node。

                 1>區別於jdk7這簡化了key的hash算法。

      ② jdk8的數組編程了node<K,V> tab,而jdk7是entry<K,V>[] tables 內部結構基本一樣,判斷取出來的頭node是否和傳進來的一樣,如果相等則賦值給局部變量e。

     ③ 看取出來的節點是不是TreeNode,如果是代表該數組對應的位置變成了紅黑樹,進行紅黑樹的插入。

     ④ 如果取出來的不是TreeNode,則代表當前依然是鏈表,for循環遍歷鏈表:

       1> 如果遍歷到一個節點爲null時 (也就是沒有key與傳進來的值相等時),則創建一個新的節點,如果已有8個node,當前進來的爲第9個,即當某一個鏈表長度大於8,則進行開始判斷是否轉換成紅黑樹:

    1、判斷如果tables[]數組長度不大於64,則將table[]數組擴容。關於槽位移動的思想是這樣的:

          將原來的一條鏈表拆成兩條鏈表,低位鏈表的數據將會到新數組的當前下標位置(原來下標多少,新下標就是多少),高位鏈表的數據將會到新數組的當前下標+當前數組長度的位置(原來下標多少,新下標就是多少+當前數組長度)。

          計算新的槽位下標是看當前hash與舊數組長度相與,結果爲0的話那麼新槽位下標還是當前的下標,如果非零,那麼新槽位下標是當前下標+當前數組長度。舉個?:hash爲1,當前數組長度爲8,1&8 爲 0,所以下一個槽位就是1;hash爲9,當前數組長度爲8,9&8 不爲 0,所以下一個槽位就是1+8 = 9。

     2、如果大於64,並且table[]數組當前位置不爲空時,則先生成一個雙向鏈表,然後在轉化成紅黑樹。

       2> 如果找到一個節點的key與入參key的相同則跳出循環。

      ⑤ 如果局部變量e不爲空(有相同key時),將新的value更新並返回舊值。

二、ConcurrentHashMap

      1) jdk7 分段鎖Segment<K,V>[]數組 + hashEntry +紅黑樹 ,通過unsafe方法+cas保證了線程安全。   

          ①初始化 ConcurrentHashMap<Object, Object> objectObjectConcurrentHashMap = new ConcurrentHashMap<>();

                 1> ssize爲segment[]數組長度,默認16,同樣要是自定義的2的冪次方。segment[]數組下hashEntry長度,同樣是2的冪次方,並且要大於,ConcurrentHashMap的長度:initialCapacity/segment[]數組長度。實際的初始化長度最小是2。

                 2> 這裏會額外初始化一個segment[0]用於後續初始化segment做參考,提高性能,通過unsafe方法操作內存放入到segment[]數組中。

      ② put方法

       1> ConcurrentHashMap 不允許存null值會報錯。

       2> 同樣根據key的hashcode,取餘計算對應的segment[]數組中的位置,不過這裏爲了保證線程安全,採用的是unsafe方法

       ③ 存值

   1>嘗試加鎖: 自旋加鎖

        while死循環嘗試加鎖,每嘗試一次retires +1,多核嘗試超過64次,則變成lock(),阻塞獲取鎖,這裏鎖住的都都是segment對象,嘗試過程中,根據hash值獲取key對應的hashEntry[]數組中的位置,如果節點位置爲null則嘗試創建。

   2> 嘗試遍歷  key對應位置的,HashEntry(也是個鏈表),如果有key值相同時則更新值。

   3> 如果遍歷過程中到達尾節點依然爲null,則創建新值,存進去(尾插法),如果超過閾值則擴容,擴容方式大概與1.8中的思路相同。

  2) jdk8 去掉了segment<K,V>[]數組的概念,只是對頭結點進行加鎖(可能是node,也肯能是treebin),同時引入了紅黑樹,複雜的是resize()和size()方法。

    1>構造方法簡化,不在初始化concurrentHashMap,不允許存放null值和null鍵。

   2>如果是鏈表和紅黑樹分別操作進行存值,這裏的Treebin其實就是代表整個紅黑樹對象,以爲紅黑樹可能進行旋轉,頭結點不固定,所以包裝成了Treebin對象進行加鎖。

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