二者最主要的區別在於Hashtable是線程安全,而HashMap則非線程安全。java
Hashtable 是不容許鍵或值爲 null 的,HashMap 的鍵值則均可覺得 null。 Hashtable在咱們put 空值的時候會直接拋空指針異常,可是HashMap卻作了特殊處理。node
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
由於Hashtable使用的是安全失敗機制(fail-safe),這種機制會使你這次讀到的數據不必定是最新的數據。編程
若是你使用null值,就會使得其沒法判斷對應的key是不存在仍是爲空,由於你沒法再調用一次contain(key)來對key是否存在進行判斷,ConcurrentHashMap同理。數組
實現方式不一樣:Hashtable 繼承了 Dictionary類,而 HashMap 繼承的是 AbstractMap 類。緩存
初始化容量不一樣:HashMap 的初始容量爲:16,Hashtable 初始容量爲:11,二者的負載因子默認都是:0.75。安全
擴容機制不一樣:當現有容量大於總容量 * 負載因子時,HashMap 擴容規則爲當前容量翻倍,Hashtable 擴容規則爲當前容量翻倍 + 1。數據結構
迭代器不一樣:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。多線程
快速失敗(fail—fast) 是java集合中的一種機制, 在用迭代器遍歷一個集合對象時,若是遍歷過程當中對集合對象的內容進行了修改(增長、刪除、修改),則會拋出Concurrent Modification Exception。併發
原理: 迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個 modCount 變量。函數
集合在被遍歷期間若是內容發生變化,就會改變modCount的值。
每當迭代器使用hashNext()/next()遍歷下一個元素以前,都會檢測modCount變量是否爲expectedmodCount值,是的話就返回遍歷;不然拋出異常,終止遍歷。
Tip:這裏異常的拋出條件是檢測到 modCount!=expectedmodCount 這個條件。若是集合發生變化時修改modCount值恰好又設置爲了expectedmodCount值,則異常不會拋出。
所以,不能依賴於這個異常是否拋出而進行併發操做的編程,這個異常只建議用於檢測併發修改的bug。
使用場景:ava.util包下的集合類都是快速失敗的,不能在多線程下發生併發修改(迭代過程當中被修改)算是一種安全機制吧。
安全失敗(fail—safe):java.util.concurrent包下的容器都是安全失敗,能夠在多線程下併發使用,併發修改。
jdk1.8前數據結構是鏈表+數組。
jdk1.8以後是鏈表+數組+紅黑樹
HashMap<String, Integer> map = new HashMap<>();
複製代碼
在jdk8前,構造方法中建立一個長度是16的 Entry[] table 用來存儲鍵值對數據的。
在jdk8之後不是在HashMap的構造方法底層建立數組了,是在第一次調用put方法時建立的數組 Node[] table 用來存儲鍵值對數據。
底層採用的key的hashCode方法結合數組長度進行無符號右移(>>>)、按位異或(^)計算hash值,按位與(&)計算出索引
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//計算索引,n爲數組長度
(n-1) & hash
//除此以外,還能夠採用:平方取中法,去語數英
複製代碼
當兩個hashCode相等時,會產生哈希碰撞,若key值內容相同則替換舊的value,否則就鏈接到鏈表的後面,鏈表長度超過閾值8轉爲紅黑樹。
1.size表示hashMap中K-V的實時數量,不是數組長度。
2.threshold(臨界值)=capacity(容量)* loadFactor(加載因子)。這個值是當前以佔據數組長的的最大值。size超過這個臨界值就會從新resize(擴容),擴容後hashMap容量是以前的2倍。
private static final long serialVersionUID = 362498820763181265L;
複製代碼
// 默認的初始容量是16 1 << 4 至關於 1*2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
複製代碼
問題:爲何必須是2的n次冪?若是輸入值不是2的冪好比10會怎麼樣?
存儲高效,儘可能減小碰撞,在(n-1)&hash求索引的時候更加均勻。 問題:若是傳入的容量默認不是2的冪
//對傳入容量進行右移位運算後進行或運算,一共進行5次或運算,能夠將當前數字中二進制最高位1的右邊所有變成1,最後+1返回
static final int tableSizeFor(int cap) {
//-1的目的是使得找到的目標值大於或等於原值
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製代碼
static final float DEFAULT_LOAD_FACTOR = 0.75f;
複製代碼
static final int MAXIMUM_CAPACITY = 1 << 30; // 2的30次冪
複製代碼
// 當桶(bucket)上的結點數大於這個值時會轉爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
複製代碼
問題:爲何Map桶中結點個數超過8才轉爲紅黑樹?
TreeNode佔用空間是普通Node的兩倍,空間和時間的權衡,同時若是爲8,log(8)=3,小於鏈表的平均8/2=4。 也就是說:選擇8由於符合泊松分佈,超過8的時候,機率已經很是小了,因此咱們選擇8這個數宇。
// 當捅(bucket)上的結點數小於這個值,樹轉爲鏈表
static final int UNTREEIFY_THRESHOLD = 6;
複製代碼
// 捅中結構轉化爲紅黑樹對應的數組長度最小的值
static final int MIN_TREEIFY_CAPACITY = 64;
複製代碼
// 存儲元素的數組
transient Node<K,V>[] table;
複製代碼
// 存放具體元素的集合
transient Set<Map.Entry<K,V>> entrySet;
複製代碼
// 存放元素的個數,size爲HashMap中K-V的實時數量,不是數組table的長度。
transient int size;
複製代碼
// 每次擴容和更改map結構的計數器
transient int modCount;
複製代碼
// 臨界值 當實際大小(容量*負載因子)超過臨界值時,會進行擴容
int threshold;
複製代碼
// 負載因子
final float loadFactor;
複製代碼
說明:loadFactor加載因子,可表示hashMap的疏密程度,影響影響hash操做到同一個數組位置的機率,默認0.75,不建議修改。
// 將默認的負載因子0.75賦值給loadFactor,並無建立數組
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
複製代碼
// 指定「容量大小」的構造函數
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
複製代碼
/* 指定「容量大小」和「負載因子」的構造函數 initialCapacity:指定的容量 loadFactor:指定的負載因子 */
public HashMap(int initialCapacity, float loadFactor) {
// 判斷初始化容量initialCapacity是否小於0
if (initialCapacity < 0)
// 若是小於0,則拋出非法的參數異常IllegalArgumentException
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 判斷初始化容量initialCapacity是否大於集合的最大容量MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
// 若是超過MAXIMUM_CAPACITY,會將MAXIMUM_CAPACITY賦值給initialCapacity
initialCapacity = MAXIMUM_CAPACITY;
// 判斷負載因子loadFactor是否小於等於0或者是不是一個非數值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
// 若是知足上述其中之一,則拋出非法的參數異常IllegalArgumentException
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// 將指定的負載因子賦值給HashMap成員變量的負載因子loadFactor
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
複製代碼
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//獲取參數集合的長度
int s = m.size();
if (s > 0) {
//判斷參數集合的長度是否大於0,說明大於0
if (table == null) { // 判斷table是否已經初始化
// 未初始化,s爲m的實際元素個數
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
// 計算獲得的t大於閾值,則初始化閾值
if (t > threshold)
threshold = tableSizeFor(t);
}
// 已初始化,而且m元素個數大於閾值,進行擴容處理
else if (s > threshold)
resize();
// 將m中的全部元素添加至HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
複製代碼
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
/* 1)transient Node<K,V>[] table; 表示存儲Map集合中元素的數組。 2)(tab = table) == null 表示將空的table賦值給tab,而後判斷tab是否等於null,第一次確定是null。 3)(n = tab.length) == 0 表示將數組的長度0賦值給n,而後判斷n是否等於0,n等於0,因爲if判斷使用雙或,知足一個便可,則執行代碼 n = (tab = resize()).length; 進行數組初始化,並將初始化好的數組長度賦值給n。 4)執行完n = (tab = resize()).length,數組tab每一個空間都是null。 */
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/* 1)i = (n - 1) & hash 表示計算數組的索引賦值給i,即肯定元素存放在哪一個桶中。 2)p = tab[i = (n - 1) & hash]表示獲取計算出的位置的數據賦值給結點p。 3) (p = tab[i = (n - 1) & hash]) == null 判斷結點位置是否等於null,若是爲null,則執行代碼:tab[i] = newNode(hash, key, value, null);根據鍵值對建立新的結點放入該位置的桶中。 小結:若是當前桶沒有哈希碰撞衝突,則直接把鍵值對插入空間位置。 */
if ((p = tab[i = (n - 1) & hash]) == null)
// 建立一個新的結點存入到桶中
tab[i] = newNode(hash, key, value, null);
else {
// 執行else說明tab[i]不等於null,表示這個位置已經有值了
Node<K,V> e; K k;
/* 比較桶中第一個元素(數組中的結點)的hash值和key是否相等 1)p.hash == hash :p.hash表示原來存在數據的hash值 hash表示後添加數據的hash值 比較兩個hash值是否相等。 說明:p表示tab[i],即 newNode(hash, key, value, null)方法返回的Node對象。 Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } 而在Node類中具備成員變量hash用來記錄着以前數據的hash值的。 2)(k = p.key) == key :p.key獲取原來數據的key賦值給k key 表示後添加數據的key比較兩個key的地址值是否相等。 3)key != null && key.equals(k):可以執行到這裏說明兩個key的地址值不相等,那麼先判斷後添加的key是否等於null,若是不等於null再調用equals方法判斷兩個key的內容是否相等。 */
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
/* 說明:兩個元素哈希值相等,而且key的值也相等,將舊的元素總體對象賦值給e,用e來記錄 */
e = p;
// hash值不相等或者key不相等;判斷p是否爲紅黑樹結點
else if (p instanceof TreeNode)
// 放入樹中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 說明是鏈表結點
else {
/* 1)若是是鏈表的話須要遍歷到最後結點而後插入 2)採用循環遍歷的方式,判斷鏈表中是否有重複的key */
for (int binCount = 0; ; ++binCount) {
/* 1)e = p.next 獲取p的下一個元素賦值給e。 2)(e = p.next) == null 判斷p.next是否等於null,等於null,說明p沒有下一個元素,那麼此時到達了鏈表的尾部,尚未找到重複的key,則說明HashMap沒有包含該鍵,將該鍵值對插入鏈表中。 */
if ((e = p.next) == null) {
/* 1)建立一個新的結點插入到尾部 p.next = newNode(hash, key, value, null); Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } 注意第四個參數next是null,由於當前元素插入到鏈表末尾了,那麼下一個結點確定是null。 2)這種添加方式也知足鏈表數據結構的特色,每次向後添加新的元素。 */
p.next = newNode(hash, key, value, null);
/* 1)結點添加完成以後判斷此時結點個數是否大於TREEIFY_THRESHOLD臨界值8,若是大於則將鏈表轉換爲紅黑樹。 2)int binCount = 0 :表示for循環的初始化值。從0開始計數。記錄着遍歷結點的個數。值是0表示第一個結點,1表示第二個結點。。。。7表示第八個結點,加上數組中的的一個元素,元素個數是9。 TREEIFY_THRESHOLD - 1 --》8 - 1 ---》7 若是binCount的值是7(加上數組中的的一個元素,元素個數是9) TREEIFY_THRESHOLD - 1也是7,此時轉換紅黑樹。 */
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 轉換爲紅黑樹
treeifyBin(tab, hash);
// 跳出循環
break;
}
/* 執行到這裏說明e = p.next 不是null,不是最後一個元素。繼續判斷鏈表中結點的key值與插入的元素的key值是否相等。 */
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循環
/* 要添加的元素和鏈表中的存在的元素的key相等了,則跳出for循環。不用再繼續比較了 直接執行下面的if語句去替換去 if (e != null) */
break;
/* 說明新添加的元素和當前結點不相等,繼續查找下一個結點。 用於遍歷桶中的鏈表,與前面的e = p.next組合,能夠遍歷鏈表 */
p = e;
}
}
/* 表示在桶中找到key值、hash值與插入元素相等的結點 也就是說經過上面的操做找到了重複的鍵,因此這裏就是把該鍵的值變爲新的值,並返回舊值 這裏完成了put方法的修改功能 */
if (e != null) {
// 記錄e的value
V oldValue = e.value;
// onlyIfAbsent爲false或者舊值爲null
if (!onlyIfAbsent || oldValue == null)
// 用新值替換舊值
// e.value 表示舊值 value表示新值
e.value = value;
// 訪問後回調
afterNodeAccess(e);
// 返回舊值
return oldValue;
}
}
// 修改記錄次數
++modCount;
// 判斷實際大小是否大於threshold閾值,若是超過則擴容
if (++size > threshold)
resize();
// 插入後回調
afterNodeInsertion(evict);
return null;
}
複製代碼
/* 替換指定哈希表的索引處桶中的全部連接結點,除非表過小,不然將修改大小。 Node<K,V>[] tab = tab 數組名 int hash = hash表示哈希值 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
/* 若是當前數組爲空或者數組的長度小於進行樹形化的閾值(MIN_TREEIFY_CAPACITY = 64),就去擴容。而不是將結點變爲紅黑樹。 目的:若是數組很小,那麼轉換紅黑樹,而後遍歷效率要低一些。這時進行擴容,那麼從新計算哈希值,鏈表長度有可能就變短了,數據會放到數組中,這樣相對來講效率高一些。 */
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//擴容方法
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
/* 1)執行到這裏說明哈希表中的數組長度大於閾值64,開始進行樹形化 2)e = tab[index = (n - 1) & hash]表示將數組中的元素取出賦值給e,e是哈希表中指定位置桶裏的鏈表結點,從第一個開始 */
// hd:紅黑樹的頭結點 tl:紅黑樹的尾結點
TreeNode<K,V> hd = null, tl = null;
do {
// 新建立一個樹的結點,內容和當前鏈表結點e一致
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p; // 將新創鍵的p結點賦值給紅黑樹的頭結點
else {
p.prev = tl; // 將上一個結點p賦值給如今的p的前一個結點
tl.next = p; // 將如今結點p做爲樹的尾結點的下一個結點
}
tl = p;
/* e = e.next 將當前結點的下一個結點賦值給e,若是下一個結點不等於null 則回到上面繼續取出鏈表中結點轉換爲紅黑樹 */
} while ((e = e.next) != null);
/* 讓桶中的第一個元素即數組中的元素指向新建的紅黑樹的結點,之後這個桶裏的元素就是紅黑樹 而不是鏈表數據結構了 */
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
複製代碼
final Node<K,V>[] resize() {
// 獲得當前數組
Node<K,V>[] oldTab = table;
// 若是當前數組等於null長度返回0,不然返回當前數組的長度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//當前閥值點 默認是12(16*0.75)
int oldThr = threshold;
int newCap, newThr = 0;
// 若是老的數組長度大於0
// 開始計算擴容後的大小
if (oldCap > 0) {
// 超過最大值就再也不擴充了,就只好隨你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
// 修改閾值爲int的最大值
threshold = Integer.MAX_VALUE;
return oldTab;
}
/* 沒超過最大值,就擴充爲原來的2倍 1) (newCap = oldCap << 1) < MAXIMUM_CAPACITY 擴大到2倍以後容量要小於最大容量 2)oldCap >= DEFAULT_INITIAL_CAPACITY 原數組長度大於等於數組初始化長度16 */
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 閾值擴大一倍
newThr = oldThr << 1; // double threshold
}
// 老閾值點大於0 直接賦值
else if (oldThr > 0) // 老閾值賦值給新的數組長度
newCap = oldThr;
else { // 直接使用默認值
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 計算新的resize最大上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 新的閥值 默認原來是12 乘以2以後變爲24
threshold = newThr;
// 建立新的哈希表
@SuppressWarnings({"rawtypes","unchecked"})
//newCap是新的數組長度--》32
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 判斷舊數組是否等於空
if (oldTab != null) {
// 把每一個bucket都移動到新的buckets中
// 遍歷舊的哈希表的每一個桶,從新計算桶裏元素的新位置
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 原來的數據賦值爲null 便於GC回收
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 { // 採用鏈表處理衝突
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 經過上述講解的原理來計算結點的新位置
do {
// 原索引
next = e.next;
// 這裏來判斷若是等於true e這個結點在resize以後不須要移動位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket裏
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket裏
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
複製代碼
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 根據hash找到位置
// 若是當前key映射到的桶不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 若是桶上的結點就是要找的key,則將node指向該結點
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 說明結點存在下一個結點
if (p instanceof TreeNode)
// 說明是以紅黑樹來處理的衝突,則獲取紅黑樹要刪除的結點
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 判斷是否以鏈表方式處理hash衝突,是的話則經過遍歷鏈表來尋找要刪除的結點
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 比較找到的key的value和要刪除的是否匹配
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 經過調用紅黑樹的方法來刪除結點
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 鏈表刪除
tab[index] = node.next;
else
p.next = node.next;
// 記錄修改次數
++modCount;
// 變更的數量
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
複製代碼
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 若是哈希表不爲空而且key對應的桶上不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
/* 判斷數組元素是否相等 根據索引的位置檢查第一個元素 注意:老是檢查第一個元素 */
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 若是不是第一個元素,判斷是否有後續結點
if ((e = first.next) != null) {
// 判斷是不是紅黑樹,是的話調用紅黑樹中的getTreeNode方法獲取結點
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 不是紅黑樹的話,那就是鏈表結構了,經過循環的方法判斷鏈表中是否存在該key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
複製代碼
在SynchronizedMap內部維護了一個普通對象Map,還有排斥鎖mutex。
若是沒有,則將對象排斥鎖賦值爲this,即調用synchronizedMap的對象,就是上面的Map。
建立出synchronizedMap以後,再操做map的時候,就會對方法上鎖
Segment 是 ConcurrentHashMap 的一個內部類,主要的組成以下:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
// 和 HashMap 中的 HashEntry 做用同樣,真正存放數據的桶
transient volatile HashEntry<K,V>[] table;
transient int count;
// 記得快速失敗(fail—fast)麼?
transient int modCount;
// 大小
transient int threshold;
// 負載因子
final float loadFactor;
}
複製代碼
HashEntry跟HashMap差很少的,可是不一樣點是,他使用volatile去修飾了他的數據Value還有下一個節點next。
原理上來講,ConcurrentHashMap 採用了分段鎖技術,其中 Segment 繼承於 ReentrantLock。
不會像 HashTable 那樣無論是 put 仍是 get 操做都須要作同步處理,理論上 ConcurrentHashMap 支持 CurrencyLevel (Segment 數組數量)的線程併發。
每當一個線程佔用鎖訪問一個 Segment 時,不會影響到其餘的 Segment。
就是說若是容量大小是16他的併發度就是16,能夠同時容許16個線程操做16個Segment並且仍是線程安全的。
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();//這就是爲啥他不能夠put null值的緣由
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
複製代碼
先定位到Segment,而後再進行put操做。
//put源碼
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 將當前 Segment 中的 table 經過 key 的 hashcode 定位到 HashEntry
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 遍歷該 HashEntry,若是不爲空則判斷傳入的 key 和當前遍歷的 key 是否相等,相等則覆蓋舊的 value。
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// 不爲空則須要新建一個 HashEntry 並加入到 Segment 中,同時會先判斷是否須要擴容。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//釋放鎖
unlock();
}
return oldValue;
}
複製代碼
首先第一步的時候會嘗試獲取鎖,若是獲取失敗確定就有其餘線程存在競爭,則利用scanAndLockForPut()
自旋獲取鎖。
MAX_SCAN_RETRIES
則改成阻塞鎖獲取,保證能獲取成功。get邏輯
get 邏輯比較簡單,只須要將 Key 經過 Hash 以後定位到具體的 Segment ,再經過一次 Hash 定位到具體的元素上。
因爲 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了內存可見性,因此每次獲取時都是最新值。
ConcurrentHashMap 的 get 方法是很是高效的,由於整個過程都不須要加鎖。
其中拋棄了原有的 Segment 分段鎖,而採用了 CAS + synchronized
來保證併發安全性。
跟HashMap很像,也把以前的HashEntry改爲了Node,可是做用不變,把值和next採用了volatile去修飾,保證了可見性,而且也引入了紅黑樹,在鏈表大於必定值的時候會轉換(默認是8)。
根據 key 計算出 hashcode 。
判斷是否須要進行初始化。
即爲當前 key 定位出的 Node,若是爲空表示當前位置能夠寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功。
若是當前位置的 hashcode == MOVED == -1
,則須要進行擴容。
若是都不知足,則利用 synchronized 鎖寫入數據。
若是數量大於 TREEIFY_THRESHOLD
則要轉換爲紅黑樹。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//計算出hash值
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) {
//爲當前key定位出的node,若是爲空表示當前位置能夠寫入數據,利用CAS嘗試寫入,失敗則自選保證成功。
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//若是當前位置的 hashcode == MOVED == -1,則須要進行擴容。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
//若是都不知足,則利用 synchronized 鎖寫入數據。
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) {
//若是數量大於 `TREEIFY_THRESHOLD` 則要轉換爲紅黑樹。
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
複製代碼
+的get操做步驟:
1.根據計算出來的 hashcode 尋址,若是就在桶上那麼直接返回值。
2.若是是紅黑樹那就按照樹的方式獲取值。
3.就不知足那就按照鏈表的方式遍歷獲取值。
CAS 是樂觀鎖的一種實現方式,是一種輕量級鎖,JUC 中不少工具類的實現就是基於 CAS 的。
CAS 操做的流程以下圖所示,線程在讀取數據時不進行加鎖,在準備寫回數據時,比較原值是否修改,若未被其餘線程修改則寫回,若已被修改,則從新執行讀取流程。
這是一種樂觀策略,認爲併發操做並不總會發生。
synchronized以前一直都是重量級的鎖,可是後來java官方是對他進行過升級的,他如今採用的是鎖升級的方式去作的。
針對 synchronized 獲取鎖的方式,JVM 使用了鎖升級的優化方式,就是先使用偏向鎖優先同一線程而後再次獲取鎖,若是失敗,就升級爲 CAS 輕量級鎖,若是失敗就會短暫自旋,防止線程被系統掛起。最後若是以上都失敗就升級爲重量級鎖。
因此是一步步升級上去的,最初也是經過不少輕量級的方式鎖定的。