Map 這樣的Key Value在軟件開發中是很是經典的結構,經常使用於在內存中存放數據。數據結構
本篇主要想討論 ConcurrentHashMap 這樣一個併發容器,在正式開始以前我以爲有必要談談 HashMap,沒有它就不會有後面的 ConcurrentHashMap。併發
衆所周知 HashMap 底層是基於數組 + 鏈表組成的,不過在 jdk1.7 和 1.8 中具體實現稍有不一樣。函數
Base 1.7性能
1.7 中的數據結構圖:this
先來看看 1.7 中的實現。spa
這是 HashMap 中比較核心的幾個成員變量;看看分別是什麼意思?code
初始化桶大小,由於底層是數組,因此這是數組默認的大小。對象
桶最大值。內存
默認的負載因子(0.75)
table真正存放數據的數組。
Map存放數量的大小。
桶大小,可在初始化時顯式指定。
負載因子,可在初始化時顯式指定。
重點解釋下負載因子:
因爲給定的 HashMap 的容量大小是固定的,好比默認初始化:
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
給定的默認容量爲 16,負載因子爲 0.75。Map 在使用過程當中不斷的往裏面存放數據,當數量達到了16 * 0.75 = 12就須要將當前 16 的容量進行擴容,而擴容這個過程涉及到 rehash、複製數據等操做,因此很是消耗性能。
所以一般建議能提早預估 HashMap 的大小最好,儘可能的減小擴容帶來的性能損耗。
根據代碼能夠看到其實真正存放數據的是
transient Entry[] table = (Entry[]) EMPTY_TABLE;
這個數組,那麼它又是如何定義的呢?
Entry 是 HashMap 中的一個內部類,從他的成員變量很容易看出:
key 就是寫入時的鍵。
value 天然就是值。
開始的時候就提到 HashMap 是由數組和鏈表組成,因此這個 next 就是用於實現鏈表結構。
hash 存放的是當前 key 的 hashcode。
知曉了基本結構,那來看看其中重要的寫入、獲取函數:
put 方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
判斷當前數組是否須要初始化。
若是 key 爲空,則 put 一個空值進去。
根據 key 計算出 hashcode。
根據計算出的 hashcode 定位出所在桶。
若是桶是一個鏈表則須要遍歷判斷裏面的 hashcode、key 是否和傳入 key 相等,若是相等則進行覆蓋,並返回原來的值。
若是桶是空的,說明當前位置沒有數據存入;新增一個 Entry 對象寫入當前位置。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
當調用 addEntry 寫入 Entry 時須要判斷是否須要擴容。
若是須要就進行兩倍擴充,並將當前的 key 從新 hash 並定位。
而在createEntry中會將當前位置的桶傳入到新建的桶中,若是當前桶有值就會在位置造成鏈表。
再來看看 get 函數:
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
首先也是根據 key 計算出 hashcode,而後定位到具體的桶中。
判斷該位置是否爲鏈表。
不是鏈表就根據key、key 的 hashcode是否相等來返回值。
爲鏈表則須要遍歷直到 key 及 hashcode 相等時候就返回值。
啥都沒取到就直接返回 null 。
做者:AI喬治 連接:https://www.jianshu.com/p/7e36a15f7d3a 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。