目录
总体介绍
HashMap是我们用于元素映射使用频率最高的数据结构,它继承自AbstractList类,并且支持一条值为null的Key和无数条value为null的数据,HashMap是线程不安全的6在多线程环境下我们通过使用Collections中的synchronizedMap使其具有线程安全的能力或者直接使ConcurrentHashMap,随着JDK的更新迭代,自jdk1.8以来,HashMap的底层数据结构已经发展为数组+链表+红黑树
HashMap元素的存储
HashMap底层使用Node<K,V>进行元素的存储,我们查看其源码:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //用来定位数组索引位置
final K key;
V value;
Node<K,V> next; //链表的下一个node
Node(int hash, K key, V value, Node<K,V> next) { ... }
public final K getKey(){ ... }
public final V getValue() { ... }
public final String toString() { ... }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
在HashMap的源码中,一个比较重要的组成部分是Node[]table,即哈希桶数组,元素根据哈希算法最终被放入哈希桶不同下标的位置中,随着元素的不断增多,可能存在两个不同的元素有相同的下标,这便是哈希冲突,哈希桶解决哈希冲突的方法是链式地址法,所谓链式地址法,是指通过采用链表+数组的方式解决哈希冲突问题,随着哈希桶内元素的不断增加,当单个链表元素数量大于等于8以及哈希桶中的总元素数量>=64时,链表就会转化为红黑树
在hashmap中添加元素
在HashMap中添加元素的逻辑如下:
1.判断数组table[i]是否为空,如果是空,则执行resize()进行扩容
2.根据键值key计算hash值得到应该插入的数组索引i,如果table[i]==null,直接创建新结点并加入数组,转向6;如果table[i]对应的元素不为空,转向3
3. table[i] の最初の要素が挿入した要素とまったく同じであるかどうかを判断します (hashcode() および equals() に従って判断)。同じ場合は値を直接上書きします。そうでない場合は 4 に進みます。
4. table[i] がtreeNodeであるかどうか、つまり、table[i]が赤黒ツリーであるかどうかを判断します。赤黒ツリーの場合は、キーと値のペアを赤黒ツリーに直接挿入します。それ以外の場合は 5 にします
5. table[i]を走査し、リンクリストの長さが8以上であるかどうかを判断し、条件が満たされる場合はリンクリストを直接赤黒ツリーに変換し、そうでない場合はリンクリストに要素を挿入します。 、そして、リンクされたリストの走査中に同じキーが値を直接上書きすることが判明した場合、それは可能です。
6. 拡張に成功した後、実際のキーと値のペアのサイズが最大容量のしきい値を超えているかどうかを判断し、最大容量のしきい値を超えている場合は、直接容量を拡張します。
HashMap のソースコードは次のとおりです。
1 public V put(K key, V value) { 2 // hashCode() of key 3 return putVal(hash(key), key, value, false, true); 4 } 5 6 Final V putVal(int hash, K key, V value, booleanonlyIfAbsent, 7 boolean evict) { 8 Node<K,V>[] tab; Node<K,V> p; int n, i; 9 // 手順①:タブが空の場合 Create 10 if ((tab = table) == null || (n = tab.length) == 0) 11 n = (tab = Resize()).length; 12 // ステップ②:インデックスを計算し、null 処理を行う 13 if ((p = tab[i = (n - 1) & hash]) == null) 14 tab[i] = newNode(hash, key, value, null); 15 else { 16 Node<K, V> e ; K k; 17 // ステップ③: ノードキーが存在するので、値を直接上書きします
18 if (p.hash == hash &&
19 ((k = p.key) == key || (key != null && key.equals(k))))
20 e = p;
21 // ステップ④:チェーンが赤黒ツリーであることを決定する
22 else if (p instanceof TreeNode)
23 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
24 // ステップ⑤: チェーンは連結リストです
25 else { 26 for (int binCount = 0; ; ++binCount) { 27 if ((e = p.next) == null) { 28 p.next = newNode(hash, key,value,null) ; //リンクされたリストの長さは 8 を超えており、29 を処理するために赤黒ツリーに変換されますif (binCount >= TREEIFY_THRESHOLD - 1) // 最初の 30 については -1 TreeifyBin(tab,ハッシュ);
31 休憩。
32 }
// keyすでに存在直接オーバー盖value
33 if (e.hash == hash &&
34 ((k = e.key) == key || (key != null && key.equals(k))) Break ;
36 p = e;
37 }
38 }
39
40 if (e != null) { // キー
41 の既存のマッピング V oldValue = e.value;
42 if (!onlyIfAbsent || oldValue == null)
43 e.value = 値;
44 afterNodeAccess(e);
45 古い値を返します。
46 }
47 }48 ++modCount;
49 // ステップ⑥: 容量を
50 拡張 if (++size > しきい値)
51size();
52 afterNodeInsertion(evict);
53 return null;
54 }
HashMapの拡張機構
JDK 1.7 と比較すると、JDK1.8 では独自の容量拡張をベースに赤黒ツリーが追加されており、その実装プロセスと条件は次のとおりです。ハッシュ テーブル内の要素がしきい値に達した場合)、拡張操作中にツリー条件が満たされたことが判明した場合(単一のリンク リスト内の要素数 >= 8&& ハッシュ バケットに格納されている総要素数 >= 64) )、リンクされたリストは赤黒ツリーに変換されます。
具体的なソースコードと解析内容は以下の通りです。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;//超过了哈希表的最大容量不进行扩容,任由其碰撞
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//数组容量扩大为两倍
newThr = oldThr << 1; // 将临界值扩大为两倍
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;//初始化容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//初始化扩容门槛
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
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 { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//新索引=原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//新的索引=原索引+oldCap
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;
}
//新索引放入bucket
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
さらに、resize() のソース コードを見ると、JDK1.8 では展開後の要素のインデックス変更も最適化されていることがわかります。観察すると、2 のべき乗の展開を使用していることがわかります (参照)長さは元の 2 倍に拡張されます)。そのため、要素の位置は元の位置、または元の位置の 2 乗に移動されます。この文の意味は次の図を見ると理解できます. n はテーブルの長さです. 図 (a) はインデックス位置を決定するために展開される前の key1 と key2 の例を示しています. 図 (b) は 2 つのキーを示しています展開後の key1 と key2 インデックス位置を決定する例 (hash1 は key1 に対応するハッシュと高次の演算結果)。
要素のハッシュを再計算すると、n が 2 倍になるため、n-1 のマスク範囲は上位ビット (赤) より 1 ビット大きくなり、新しいインデックスは次のように変更されます。
したがって、HashMapを展開する際には、JDK1.7の実装のようにハッシュを再計算する必要はなく、元のハッシュ値の新たに追加したビットが1か0かを確認するだけで済みます。インデックスは変更されていません。1 の場合、インデックスは「元のインデックス + oldCap」になります。下の図では、16 を 32 に拡張したサイズ変更の概略図がわかります。
この設計は非常に独創的であると言えます。JDK1 の拡張後にハッシュを再計算するプロセスが省略されており、以前にハッシュ競合が発生したノードを新しいバケットに効果的に均等に分散できます。
HashMap のスレッド セーフ
ハッシュマップのスレッドの安全性の低下は、主に次の 4 つの側面に反映されます。
1. 要素の追加と削除に不安がある
同時環境では、複数のスレッドが要素の追加、削除、変更などの HashMap を同時に変更すると、データ構造内で競合が発生し、最終的にはデータの不整合や損失につながる可能性があります。
これは、複数のスレッドが同じバケット内のデータを同時に変更しようとする可能性があり、変更プロセス中に一部のデータが失われたり、他のスレッドによって変更されたデータが上書きされたりして、データの不整合が発生する可能性があるためです。
2. 拡張運営に不安がある
HashMapは一定の容量に達すると拡張する必要があり、拡張時にはハッシュ値の再計算や保存場所の再割り当てなどの作業が必要になります。拡張プロセス中に複数のスレッドが同時に挿入または削除操作を実行すると、データ構造が混乱したり、無限ループやその他の問題が発生したりする可能性があります。
3. ハッシュ衝突は安全ではありません
配列を挿入または削除すると、ハッシュの競合が発生する可能性があります。つまり、異なるキーと値のペアが配列内の同じ位置にマップされる可能性があり、競合を解決するにはリンク リストまたは赤黒ツリーが必要です。
ただし、複数のスレッドが同時に挿入や削除の操作を行うと、リンクリストや赤黒ツリーの構造が破壊され、データの損失や異常が発生する可能性があります。
4. スレッド間の非可視性はセキュリティ上の問題につながる
HashMap はスレッドセーフではないため、複数のスレッドが同じ HashMap インスタンスに同時にアクセスする可能性があります。
スレッドが HashMap を変更すると、他のスレッドはこれらの変更をすぐに確認できない可能性があり、データの不整合が生じる可能性があります。
HashMap のスレッド安全性の問題を解決するには、ロック セグメンテーション テクノロジ、CAS、および揮発性変数を使用してスレッドの安全性とデータの可視性を確保する ConcurrentHashMap を使用できます。
ただし、これらの手法では追加のオーバーヘッドも発生するため、同時実行性が高くないシナリオでは、HashMap の方が ConcurrentHashMap よりも高速になる可能性があります。