JAVA基礎——00.HashMap、HashTable、ConcurrentHashMap的區別

#一、HashMap
基於哈希表實現的,每一個元素是一個key-value對,內部通過單鏈表解決衝突問題。容量不足時,會自動增長,並且HashMap的長度總是2的n次方。
是非線程安全的。
實現了Serializable接口,支持序列化。實現了Cloneable接口,能被克隆。
##存數據的過程:
內部是一個存數據的Entry數組,HashMap採用鏈表解決衝突,每一個Entry本質是一個單向鏈表。數組中的元素是單鏈表的頭結點。
當準備添加一個key-value時:

  1. 首先通過hash(key)方法計算hash值;
  2. 然後通過indexFox(hash,length)求給key-value對的位置(計算方法:先用hash&0x7FFFFFFF後,再對length取模,這就保證每一個key-value對都能存入HashMap中,當計算出的位置相同時,由於存入位置是一個鏈表,則把這個key-value對插入立案表頭。)
    注意:
    1.key與value都允許null。key爲null的鍵值對永遠放在以table[0]爲頭結點的鏈表中。
    2.存儲數據的數組默認是16.HashMap有自己的擴容機制:

變量size:記錄HashMap的底層數組中一用槽的數量
變量threshold:是HashMap的閾值,用於判斷是否需要調整HashMap的容量(threshold=容量*加載因子)
變量DFEAULT_LOAD_FECTOR=0.75f:加載因子爲0.75

##擴容條件:
當size>threshold時,對HashMap擴容。

###擴容
是新建一個HashMap的底層數組,然後調用transfer方法,將HashMap的全部元素添加到新的HashMap中。
####JDK1.7中:
new一個新的數組,然後對old元素rehash,複製加入
####JDK1.8中:
改進了resize()方法:
1、首先判斷是初始化還是擴容——擴容時,先看Capacity變量若未大於max,則令閾值左移1位(擴爲2倍)
2、然後new一個table,將原數組放入,放入的原則是元素要麼在原位置,要麼是在原位置再移動2次冪的位置具體實現爲——rehash元素,在高位多1bit,若爲0,索引不變;若爲1,地址變爲原索引+oldCapaccity.
##HashMap的構造方法:
注意:使用HashMap時,最好預估元素的個數,有助於提高HashMap的性能。
共有四個構造方法:設計兩個重要參數:初始容量和加載因子,默認是16和0.75.(加載因子越大,對空間的利用更充分,但是查找效率會低,一般不要修改加載因子)
構造方法會將實際容量設爲不小於指定的容量的2的次方的一個數,且最大值不能超過2的30次方。
#二、Hashtable
1、同樣基於哈希表實現,每個元素同樣是一個key-value對。內部也是通過單鏈表解決衝突問題。容量不足時,會自動擴容
2、是JDK1.0引入的類,是線程安全的,用於多線程環境中
3、同樣實現了Serializable接口,支持序列化,實現了Cloneable接口,能被克隆
##HashMap與Hashtable的區別:
1、繼承的父類不同
HashMap繼承AbstractMap類,Hashtable繼承自Dictionary類。都實現了Map接口
2、線程安全性不同
HashMap不是線程安全的——在多線程下使用時,需要增加同步處理。可以通過對自然封裝該映射的對象進行同步操作來實現,如:
在創建時完成這一操作

Map m=Collections.synchronizedMap(new HashMap(...));

Hashtable中的方法是Synchronized的,所以是線程安全的。
3、是否提供contains方法
HashMap用containsValue和containsKey替換了contains方法。
HashTable三個contains方法都有,其中,contains與containsValue功能相同。
4、key和value是否允許null值
hashtable不允許key和value有null
hashmap允許key和value有null,只能一個鍵值是null,可以有多個value是null.
5、兩個內部遍歷方法不同
都使用了Iterator,hashtable還使用了Enumeration
6、hash值不同
hashtable直接使用對象的hashCode,求位置索引時用取模運算;
HashMap重新計算了hash值,求位置索引時用與運算,一般先用hash&0x7FFFFFFF後,再對length取模
7、內部實現使用的數組初始化和擴容方式不同
HashMap默認是16,要求擴容一定是2 的整數次冪,擴容時變爲原來容量的2倍
hashtable默認是11,不要求一定是2的整數次冪,擴容時變爲原來容量的2倍加1

###HashMap爲什麼是線程不安全的?
底層是一個哈希表,當發生衝突時,HashMap採用鏈表的方式來解決的,在對應的數組位置存放鏈表的頭結點。新加入的結點會從頭結點加入。
在多線程訪問時:
####1)在put操作時,會調用addEntry方
兩個線程同時讀同一個數組位置調用addEntry方法時,兩個線程會同時得到現在的頭結點。然後線程A寫入新結點,線程B也寫入新結點,那麼會造成B操作覆蓋A操作,A寫入的新結點丟失
####2)刪除鍵值對
多個線程同時操作一個數組位置時,也都會先獲取現在狀態下的頭結點,之後再把結果寫回到數組位置,但是其他線程可能已經修改了這個位置,此時就會覆蓋其他線程的修改
####3)擴容時
當多個線程同時檢測到一個HashMap需要擴容時,會同時resize,此時一個線程可能會覆蓋其他線程resize的結果
#三、ConcurrentHashMap
ConcurrentHashMap繼承AbstractMap,實現ConcurrentMap、Serializable接口

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {

##3.1 JDK6/7中主要使用鎖段(segment)來實現減小鎖粒度,把hashmap分割成若干個鎖段。
1)put時:
需要鎖住Segment
2)get時:
不需要加鎖,使用volatile來保證可見性
##3.2 JDK7中,當長度過長碰撞會很繁瑣,鏈表的增改刪查會影響性能。
JDK8中完全重寫了CurrentHashMap,主要變化有:
1、將Segment改爲node,來減小鎖粒度
2、設計了MOVED狀態,當resize中,線程2put時,會幫助resize
3、使用了三個CAS操作來確保node的一些操作的原子性,代替鎖
4、sizeCtl的不同值來代表不同含義,起到控制作用

底層仍然使用數組+鏈表+紅黑樹實現
##3.3 sizeCtl屬性:
取值不同,代表不同的含義:
1、負數:正在進行初始化或擴容
2、-1代表正在初始化
3、-N代表有N-1個線程正在進行擴容操作
4、正數或0代表hash表沒有被初始化,這個數值表示初始化或下一次進行擴容的大小
##3.4 Node類:
包裝了key-value鍵值對,與HashMap不同的是對value和next屬性設置了volatile同步鎖(與JDK7的Segment相同),不允許調用setValue方法直接改變Node的value,增加find方法輔助map.get()方法。

static class Node<K,V> implements Map.Entry<K,V> 
...
Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }

##3.5 TreeNode類:
當鏈表長度過長時,會轉換爲TreeNode。與HashMap不同的是:不是直接轉換爲紅黑樹,而是包裝爲TreeNode放在TreeBin對象中,有TreeBin完成紅黑樹的包裝。帶有next指針。
##3.6 TreeBin類:
包裝的是TreeNode結點。代替TreeNode的根結點,在實際的ConcurrentHashMap的數組中,存放的是TreeBin對象,而不是TreeNode對象,這是與HashMap的區別。並帶有讀寫鎖。
##3.7 三個核心方法:

    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

U.getObjectVolatile、U.compareAndSwapObject、U.putObjectVolatile是ConcurrentHashMap中定義的三個原子操作,用於針對指定位置的節點進行操作。其中,U.compareAndSwapXXX,這個方法是利用一個CAS算法實現無鎖化的修改值的操作,可以大大降低鎖代理的性能消耗。
CAS算法的基本思想是:
不斷地比較當前內存中的變量值與你指定的一個變量值是否相等,如果相等,則接受你指定的修改的值,否則拒絕你的操作,因爲此時當前線程中的值已經不是最新的值。(與樂觀鎖、SVN的思想比較類似)。
##3.8 擴容方法:
transfer:與HashMap很像,支持併發擴容,但沒有加鎖!!!!整個擴容操作分爲兩部分:
第一部分:構建一個nextTable,容量是原來的兩倍,是在單線程下完成的。
第二部分:將原來table中的元素複製到nextTable中,允許多線程操作。
整體流程如下:
首先定義不允許key或value爲null的情況放入。
對於每一個放入的值,首先利用spread方法對key的hashCode進行一次hash計算,由此來確定這個值在table中的位置。
如果位置是空,直接放入,不需要加鎖
如果位置存在結點,發生了hash衝突——先判斷節點的類型——是鏈表節點,則得到的結點就是hash值相同的節點組成的鏈表的頭結點。需要依次向後遍歷確定新加入的值的位置。如果有key值相同的,就更新value;否則在鏈表尾部插入新結點。如果加入新結點後鏈表長度大於8,就把鏈表轉換成紅黑樹。如果這個節點的類型已經是樹節點的話,直接調用樹節點的插入方式插入新的值。

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