文章目錄
一、 前言
本文基本是自己看完之後的一個總結記錄,所以寫的很混亂,很多語言的描述也並不清晰。
推薦 : https://blog.csdn.net/u012403290/article/details/68488562 講的比我要清晰多了。本文只作爲個人記錄使用。
二、HashMap
1. Node
HashMap 底層實現是通過一個內部類數組 transient Node<K,V>[] table;
這裏Node是個自定義內部類如下,可以看出來Node 的本質是一個單向鏈表。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
... 其它代碼
}
四個屬性意義分別如下:
hash : 保存key的hash值
key: 保存節點的key值
value: 保存節點的value值
next: 指向下一個Node節點
HashMap結構如圖(手工畫,略醜)
爲了方便描述,我們將數組上的每一個元素和所鏈接的元素鏈表或樹稱爲桶。如Node A0、Node A1、Node A2 這樣一個結構稱爲桶。將NodeA0、A1稱爲桶的節點
2、put 方法
註釋寫的比較詳細,寫了很多次都沒寫出來一個好點的例子。
// 保存數據的的Node數組
transient Node<K,V>[] table;
/**
* 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) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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. 如果 Node 數組還沒初始化,則進行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 如果key的hash處理後所對應的table數組位置的桶還沒有初始化(table[i] = null, 說明table的第i個位置還沒有Node節點,所以說桶還沒有初始化),則創建新節點並插入,作爲當前位置桶的第一個節點
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 3. 判斷如果key值等於當前桶節點的key值,則記錄下節點(e = p)。留待後面處理
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4. 如果當前桶已經是轉化爲紅黑樹結構,則以紅黑樹規則插入節點
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5. 到這裏說明, 當前位置存在桶結構(即存在Node節點),且當前位置尚不構成紅黑樹結構
for (int binCount = 0; ; ++binCount) {
// 6. 如果p節點就是最後一個節點(p.next = null), 就初始化e節點,並添加在p節點後(因爲p 節點已經是最後一個節點,所以當前桶中沒有當前節點,新建節點,插入末尾)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 7. 如果當前桶節點數量大於等於7。則轉換成紅黑樹結構。小於等於6時恢復成鏈表(爲了保證查找效率,在連接結構大於等於7的情況下會轉換爲樹結構)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 8. 如果找到了匹配了當前key的hash的節點。跳出循環,進行value賦值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 9. 將e 賦值給 p (進行下一個節點的判斷)
p = e;
}
}
// 10. 如果 e 不爲空,則說明找到了對應key的桶元素
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 11. 進行新的value賦值,並返回舊value值 - onlyIfAbsent 在put 方法中恆定傳false
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 12. 記錄map修改次數,在快速失敗時使用
++modCount;
// 13. 計算如果新增後的大小超過閾值,則重新設置大小
if (++size > threshold)
resize();
// 14. 進行插入的後操作,供子類實現
afterNodeInsertion(evict);
return null;
}
// 在 remove -> removeNode -> removeTreeNode 方法中判斷是否解除樹化
3、get 方法
get 方法相較於put方法更加簡單
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果table不爲空,且對應的桶不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果 找到對應keyHash的值,則返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果下面一個節點不爲空,則 讓
if ((e = first.next) != null) {
// 如果是紅黑樹,則按照紅黑樹的邏輯查找節點
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 否則桶一直往下遍歷
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
4、entrySet 方法的遍歷
-
在HashMap 中有一種遍歷方式如下
HashMap<String, String> hashMap = new HashMap(); hashMap.put("A", "1"); hashMap.put("B", "1"); Set<Map.Entry<String, String>> set = hashMap.entrySet(); for (Map.Entry<String, String> entry : set) { System.out.println(entry.getKey()); System.out.println(entry.getValue()); }
這是一種很常見的遍歷方式,我們點進去entrySet方法,看到如下。
我們知道HashMap 中所有的數據都存放在Node[] 數組中,那麼這個 entrySet是如何實現遍歷整個Map的呢?
可以看到entrySet方法中初始化了 entrySet 變量。我們進入EntrySet類中發現並無其他。transient Set<Map.Entry<K,V>> entrySet; public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; } final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator<Map.Entry<K,V>> iterator() { return new EntryIterator(); } public final boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Node<K,V> candidate = getNode(hash(key), key); return candidate != null && candidate.equals(e); } public final boolean remove(Object o) { if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Object value = e.getValue(); return removeNode(hash(key), key, value, true, true) != null; } return false; } public final Spliterator<Map.Entry<K,V>> spliterator() { return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer<? super Map.Entry<K,V>> action) { Node<K,V>[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) action.accept(e); } if (modCount != mc) throw new ConcurrentModificationException(); } } }
-
這時需要注意的是: **forEach 只是一種語法糖,其底層是通過迭代器實現的。在反編譯後的代碼其實是迭代器實現。**所以我們的遍歷代碼在編譯後其實是下面這種形式。可以看到他調用的是 iterator() 方法。
Iterator<Map.Entry<String, String>> iterator = set.iterator();
while (iterator.hasNext()){
Map.Entry<String, String> next = iterator.next();
System.out.println(next.getKey());
System.out.println(next.getValue());
}
- 所以我們進入EntrySet.iterator()方法中,EntrySet.iterator()中只初始化了一個 EntryIterator() ,這也是個HashMap 內部類。再進去EntryIterator類中,發現EntryIterator 類繼承了HashIterator 類,再進去 HashIterator 類中。 所以整個過程是 EntrySet -> EntryIterator -> HashIterator。 這幾個類都是HashMap 內部類。
EntryIterator 源碼如下 ,下面可以看到,next方式調用的是父類的nextNode 方法,即HashIterator.nextNode 方法
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
- HashIterator 代碼如下,我們可以就豁然開朗了,註釋都在代碼中。
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
// 初始化的時候將table賦值給t
Node<K,V>[] t = table;
current = next = null;
// 設置順序從0開始
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
// 當我們調用next 方法時,就會調用這個方法。這個方法的作用就是將獲取下一個節點並返回。
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
二、HashTable 實現線程安全
HashTable 實現 線程安全主要是通過加鎖(synchronized)。通過鎖住整個數組結構來保證線程安全,除此之外,實現基本和HashMap 相同,需要注意的是,HashTable中沒有使用紅黑樹結構,全部使用鏈表結構。
1、 Entry 類
這裏的Entry 和 HashMap 中的Node 相同,就是換了個名字,換湯不換藥。
2、 put 方法
這個比HashMap 還簡單
public synchronized V put(K key, V value) {
// 非空判斷
if (value == null) {
throw new NullPointerException();
}
// 確保key值還未保存在 table 數組中
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 遍歷查找,如果找到對應的key值,則將value替換
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
// 否則添加新的節點
addEntry(hash, key, value, index);
return null;
}
3、 get 方法
get 方法更加簡單。
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
// 找到下標
int index = (hash & 0x7FFFFFFF) % tab.length;
// 找到對應桶,遍歷節點
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
三、ConcurrentHashMap
HashMap 由於加了全局鎖,會導致併發情況下效率低下,相比較而言 ConcurrentHashMap 效率要高得多。相較而言ConcurrentHashMap 也可以保證線程安全。ConcurrentHashMap 的思想是分段鎖,而不是像HashTable一樣的全局鎖。 在Jdk 1.7 和 Jdk1.8中 ConcurrentHashMap 的實現是不同的。這裏主要介紹Jdk1.8。所以只是簡單提一下Jdk1.7。
在Jdk1.7 中 :ConcurrentHashMap 引入片段(Segment)概念, 每若干個桶都有一個片段鎖,各個片段鎖不衝突。獲取數據時先獲取當前桶的所屬片段的片段鎖。
在Jdk 1.8 中:ConcurrentHashMap 對分段鎖的更細緻的劃分,每個桶都有一個獨立的鎖。不再使用segment,使用了 CAS 來實現單獨的桶鎖。synchronized 實現單獨的桶鎖。核心思想是CAS。
1. CAS(Compare And Swap)
1.1. 概念
CAS 即 比較並交換。他是一條CPU併發原語。功能是判斷內存某個位置上的值是否爲預期值,如果是則更改爲新的值,這個過程是原子的。CAS併發原語體現在JAVA語言中就是sun.misc.Unsafe類中的各個方法。調用UnSafe類中的CAS方法, JVM會幫我們實現CAS 彙編指令。這是一種完全依賴於硬件的功能,通過它實現了原子操作。再次強調,由於CAS是一種系統原語,原語屬於操作系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致問題。
1.2 核心類 UnSafe
UnSafe是CAS的核心類,由於Java方法無法直接訪問底層系統,需要通過本地(native) 方法來訪問,Unsafe相當於一 一個後門,基於該類可以直接操作特定內存的數據。Unsafe類存在於sun.misc包中, 其內部方法操作可以像C的指針一樣直接操作內存,因爲Java中CAS操作的執行依賴於Unsafe類的方法。
2. ConcurrentHashMap
2.1 ConcurrentHashMap 3個原子性操作方法。
// 根據Volatile特性, 獲取到最新的table數組的第i個node。
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);
}
// 四個參數分別是: 操作對象,偏移量,期待值,新值。
// 利用CAS實現如下操作: 取出 tab數組的第i個Node元素,比較是否和c相等,相等則將c替換成V。這個操作線程安全
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);
}
// 根據Volatile特性, 設置tab數組的第i個node,立即可見。
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
2.2 put 方法
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p>The value can be retrieved by calling the {@code get} method
* with a key that is equal to the original key.
*
* @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 {@code key}, or
* {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
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;
// 初始化Node 數組
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 獲取 Node數組某元素,如果爲空,則創建新節點插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 通過CAS進行賦值新節點並插入Node數組中
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;
// 鎖住某一個桶的頭結點進行操作 --> 只是鎖住了一個頭結點。HashTable 鎖住了整個Node[],效率可想而知
synchronized (f) {
// 如果新插入節點屬於f桶,則進入f桶中查找合適節點
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 查找到key相同的節點,替換value值
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) {
// 這裏是大於等於8進行樹形轉換,小於等於6切換回鏈表
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
2.3 get 方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
// 如果對應桶不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果e節點hash值和key相同,則返回value
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 小於0就說明已經再擴容或者已經在初始化
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 如果還沒達到桶末尾,則往下繼續查找
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
三、總結
HashMap
、ConcurrentHashMap
存儲結構是 數組+鏈表和紅黑樹 來存儲數據,HashTable
使用數組+鏈表 來存儲數據。HashTable
通過synchronized
在某些方法上加鎖來實現線程安全。同時也使得效率變低ConcurrentHashMap
實現線程安全的原理是CAS
。通過synchronized
來鎖住桶的第一個節點(鎖住第一個節點後其餘線程也就無法訪問這個桶了)來實現線程安全。鎖的顆粒度更細,所以效率更高。- 在JDK1.7 中 使用了片段(
segment
)來加鎖, 一個片段鎖住若干個桶,相較於HashTable
鎖的顆粒度更細,但是在JDK1.8中捨棄了segment
,通過CAS
和synchronized
爲每個桶都加了一個鎖,顆粒度更高,效率也更高。 - 在
HashMap
中,鏈表長度大於等於7時會轉換爲樹結構,小於等於6時會轉換爲鏈表;在ConcurrentHashMap
中是大於等於8時轉換爲樹結構,小於等於6時轉化爲鏈表;在HashTable
中沒有使用紅黑樹的結構。
以上:內容部分自己總結
如有侵擾,聯繫刪除。 內容僅用於自我記錄學習使用。如有錯誤,歡迎指正