あなたは治療法の病気の源は表示されません--- HashMapのソースコード解析

注目点は、迷子にしないでください。Java関連のテクノロジーと継続的に更新情報!
あなたは賞賛にそれを指していない~~~

HashMapのは、地図インターフェースクラスハッシュテーブルの実装に基づいています。この実装では、地図関連の操作の全てを提供し、キーのNULL値を使用することがnullであることができます。(ハッシュマップを除き、実質的に同一のハッシュテーブルハッシュマップは、同期、そしてそれはあなたがキーと値をゼロにすることができません。)、また、内部HashMapの素子配置は順不同です。

ハッシュ関数素子が合理各ハッシュバケットに分散させることができると仮定し、その後のHashMapプットの効率は、他の基本的な操作は高くなり得る(複雑さのレベルの時定数は、O(N)です)。その容量(ハッシュ・バケットの数)との和に比例したサイズ(キーと値のペアの数)を持つすべての要素HashMapのインスタンスの時間反復。あなたがHashMapのの繰り返しパフォーマンスを懸念しているのであれば、それは非常に高い初期容量、負荷係数を設定するか、非常に低く設定するべきではありません。
ここに画像を挿入説明

グループ交換へようこそ

初期容量と負荷係数:HashMapの一例は、2つのパラメータがその性能に影響を与えています。容量は、ハッシュテーブルの初期サイズを作成する際、ハッシュテーブルのバケットの数を、初期容量が指定されている意味します。負荷率は、全能力でどの程度のハッシュテーブルは、ハッシュ・テーブルは、自動拡張すべきときに測定するために使用される尺度です。ハッシュテーブル内の要素の数が負荷係数との積と、ハッシュテーブルの現在の容量を超えたときにハッシュを(再ハッシュ)再計算される(すなわち、内部データ構造の再構成)、元の2回の予定にハッシュテーブルのバケットの多数回。

一般的に、時間コストとスペースコストの間で、0.75の負荷率にデフォルト値を設定すると、比較的良好なトレードオフです。この値が少し高くなっているスペースのオーバーヘッドを減少させるが(getおよびputを含むほとんどの操作にHashMapクラスに反映され、)ルックアップコストを増やすことができます。我々は初期容量を設定すると、それによって焼き直しの操作の数を減少させる、負荷及び負荷率を意図要素の合理的な数とみなされるべきです。負荷率(初期容量>最大エントリ/負荷率)で割ったエントリの最大数よりも大きい初期容量場合、リロード動作は発生しません。

ハッシュマップのインスタンスが要素(キーと値のペア)の多くを保存する必要がある場合、それはHashMapのストレージ効率は、自動膨張よりもはるかに高い作成できるように、十分に大きな容量を指定します。

使用するキーの多くの()メソッドの結果が同じであれば、ハッシュテーブルのパフォーマンスが非常に遅くなりますハッシュコードを注意してください。キーが同等であるときに衝撃を向上させるために、HashMapの効率を改善するために結合のこれらの並べ替えを使用します。

HashMapのは同期されませんのでご注意ください。複数のスレッドがHashMapをアクセスすると、少なくとも1つのスレッド構造変化が発生した場合には、外部で同期をとる必要があります。(増加したり、キーと値のペアの削除は、ソース属性に具現化される任意の構造的変化を参照するmodCount動作を構造変化されていないキーに対応する唯一の修正値を生じる変化します)。同期させることにより、外部同期は、一般的に、オブジェクトの完全なマップをカプセル化します。

そのようなオブジェクトの場合は、Collections.synchronizedMap同期マップを変換するにはマップを使用することができ、このアクションは最高のマップへの偶発的な非同期アクセスを防ぐために変換する前に、作成時に完了しています。

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

すべての関連メソッドのHashMapのイテレータセットフェイルファストている(フェイルファスト):イテレータが作成された場合、反復子自体のremoveメソッドに加えて、構造変化の発生をマッピングし、反復子はConcurrentModificationExceptionをスローします。したがって、同時変更の顔に、迅速かつきれいにイテレータは、あらゆるリスクを取らずに失敗します。

フェイルファストことイテレータ特性を非同期の同時変更は、ハードと高速保証するものではありませんときに注意してください。フェイルファスト反復子はConcurrentModificationExceptionをスローするために最善を尽くします。そのため、プログラムの正しさを保証するためにこの例外に依存しているの準備が間違っている:反復子のフェイルファストの動作は、エラーのみを検出するために使用する必要があります。

コンストラクタ

4の合計HashMapのコンストラクタ:

  • 引数なしのコンストラクタ、16のデフォルトの初期容量、デフォルトの負荷係数0.75

  • 指定された初期容量、デフォルトの負荷係数0.75

  • 指定された初期容量と負荷係数

  • マップ構造渡すことによって、
    1,2,4は、既存の地図HashMapを構築するだけの簡便な方法で第四、コンストラクタの最初の三種類を呼び出し、その3,4は、ここで二つの構成機能を実現する焦点になりますしたが。

public class HashMap<K,V> 
 	extends AbstractMap<K,V> 
 	implements Map<K,V>, Cloneable, Serializable { 
 	
	//......
	
	// 空表
 static final Entry<?,?>[] EMPTY_TABLE = {};
 // 哈希表
 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
	
 // 容器扩容阈值,当容器大小(size)达到此值时,容器就会扩容。
 // size = 容量 * 负载因子
 // 如果table == EMPTY_TABLE,那么就会用这个值作为初始容量,创建新的哈希表
 int threshold;
 
 // 负载因子
 final float loadFactor;
 // 构造函数3:指定初始容量和负载因子
 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();
 }
 // 构造函数4:用传入的map构造一个新的HashMap 
 public HashMap(Map<? extends K, ? extends V> m) {
 this(Math.max((int) ( m.size() / DEFAULT_LOAD_FACTOR) + 1,
 DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
 // 分配哈希表空间
 inflateTable(threshold);
 putAllForCreate(m);
 }
 
 //......
 
}

上記ソースコードは、注意点:

  1. 既定のしきい値は容量、16の初期の展開と同じです。ハッシュテーブルが空である場合、ハッシュマップは、内部に内蔵されたハッシュテーブルの初期容量として閾値は、物質は、ハッシュテーブルアレイであろう
  2. inflateTable方法は、(「膨張」手段として翻訳膨張、それは後に詳述する)、ハッシュテーブルを作成するために、メモリ空間の割り当てテーブルの動作です。しかし、コンストラクタは、初期容量と負荷係数を指定し、すぐinflateTableを呼び出しません。すべてのソースコード内の場所を探すinflateTableをしている呼び出します。
graph LR
HashMap构造函数-Map为参数 --> inflateTable
put --> inflateTable
putAll --> inflateTable
clone --> inflateTable
readObject --> inflateTable

予備的な外観は、唯一の引数リストは、MapコンストラクタはinflateTableを呼び出しているが、内部のHashMap(地図マップ)コンストラクタロジックは、(loadFactorフロート、int型InitialCapacityの値)HashMapのそれを呼び出すことですコンストラクタは再び、容量と負荷係数を終え初期化コールinflateTable。だから、少し要約:HashMapのは、すぐに初期化フェーズでハッシュテーブルを作成しません。

コールロジック

呼び出しコードをより良く理解するために、リスト図の方法の間に呼び出し関係:
ここに画像を挿入説明
内部データ構造

:内部データ構造は、以下に示すように、HashMapの+は、エントリHashMapの静的内部クラスに格納された各キーに対して、構造配列リストで維持ここに画像を挿入説明
プットを実現します

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<K,V> 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;
 }
 void addEntry(int hash, K key, V value, int bucketIndex) {
 // 如果容器大小大于等于阈值,且目标桶的entry不等于null
 if ((size >= threshold) && (null != table[bucketIndex])) {
 // 容器扩容: 哈希表原长度 * 2
 resize(2 * table.length);
 // 重新计算键的哈希值
 hash = (null != key) ? hash(key) : 0;
 // 重新计算哈希值对应存储的哈希表的位置
 bucketIndex = indexFor(hash, table.length);
 }
 createEntry(hash, key, value, bucketIndex);
 }
  1. メソッド内で入れて最初のキースペースを保存することができ、ハッシュテーブルは、テーブルが空の場合、ハッシュテーブルはテーブルが構築され、(上記の内部データ構造体の配列を)確立され、空でないと判断はい。
  2. キー値ペアを格納するには、ハッシュキーコードはint型の値に応じて返される計算、コード(ハッシュ)をハッシュする必要があり、その後、ハッシュコードに基づいて、固定長配列(インデックス)に格納された位置を算出します
  3. 添字を得た後、ハッシュテーブルに格納されている場所を見つける必要があります。HashMapのは、最初のエントリが空でない場合は指定されたエントリオブジェクトは、インデックスに格納されているロードされ、それは、エントリと、ハッシュキー(==との比較とを比較すると等しいキー)以上です。あなたはその後、プット・ハッシュ、キーマッチ、エントリに値をカバーし、付属している場合は、直接古い値を返し、そうでない場合は、これまでの最後のエントリまで、次のエントリのエントリポイントを探してください。
  4. 如果HashMap加载指定下标中存放的Entry对象是null,又或者是找完整条Entry链表都没有匹配的hash和key。那么就调用addEntry新增一个Entry
  5. addEntry方法中会做一些前置处理。HashMap会判断容器当前存放的键值对数量是否达到了设定的扩容阈值,如果达到了就扩容2倍。扩容后重新计算哈希码,并根据新哈希码和新数组长度重新计算存储位置。做好潜质处理后,就调用createEntry新增一个Entry。
  6. 由于上面已经做了前置的处理,createEntry方法就不用担心扩容的问题,放心存Entry即可。该方法会在给定的下标为止存放put进来的key,value,当然这个key,value是包装在Entry中的,让后将Entry指向旧的Entry。

建哈希表的逻辑(inflateTable)

建哈希表是在inflateTable方法中实现的:

	/**
 * 将一个数换算成2的n次幂
 * @param number
 * @return
 */
 private static int roundUpToPowerOf2(int number) {
 // assert number >= 0 : "number must be non-negative";
 return number >= MAXIMUM_CAPACITY
 ? MAXIMUM_CAPACITY
 : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
 // 理解 Integer.highestOneBit((number - 1) << 1)
 // 比如 number = 23,23 - 1 = 22,二进制是:10110
 // 22 左移一位(右边补1个0),结果是:101100
 // Integer.highestOneBit() 函数的作用是取左边最高一位,其余位取0,
 // 即:101100 -> 100000,换成十进制就是 32
 }
 /**
 * inflate有“膨胀”、“充气”的意思。
 * 理解为初始化哈希表,分配哈希表内存空间
 */
 private void inflateTable(int toSize) {
 // Find a power of 2 >= toSize
 // 找出大于等于toSize的2的n次幂,作为哈希表的容量
 int capacity = roundUpToPowerOf2(toSize);
 // 计算新的扩容阈值: 容量 * 负载因子
 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
 // 指定容量建哈希表
 table = new Entry[capacity];
 // 根据容量判断是否需要初始化hashSeed
 initHashSeedAsNeeded(capacity);
 }

理解一下roundUpToPowerOf2方法:

roundUpToPowerOf2部分计算结果:
roundUpToPowerOf2(0) = 1
roundUpToPowerOf2(1) = 1
roundUpToPowerOf2(2) = 2
roundUpToPowerOf2(3) = 4
roundUpToPowerOf2(4) = 4
roundUpToPowerOf2(5) = 8
roundUpToPowerOf2(6) = 8
roundUpToPowerOf2(7) = 8
roundUpToPowerOf2(8) = 8
roundUpToPowerOf2(9) = 16
roundUpToPowerOf2(10) = 16
roundUpToPowerOf2(11) = 16
roundUpToPowerOf2(12) = 16
roundUpToPowerOf2(13) = 16
roundUpToPowerOf2(14) = 16
roundUpToPowerOf2(15) = 16
roundUpToPowerOf2(16) = 16
roundUpToPowerOf2(17) = 32
roundUpToPowerOf2(6)计算示例:
计算公式:Integer.highestOneBit((5 - 1) << 1)
计算5<<1:
 00000101
<<1
-------------
 00001010
 
1010的十进制是10,然后计算Integer.highestOneBit(10),
该函数的作用是取传入数值的最高位然后其余低位取0,
所以Integer.highestOneBit(10)应该等于二进制的1000,即8

值得注意的是,inflateTable中最后还调用了一个initHashSeedAsNeeded(capacity)方法,该方法是用来依据容量决定是否需要初始化hashSeed,hashSeed默认是0,如果初始化hashSeed,它的值将会是一个随机值。

Alternative hashing与hashSeed

在源码中有一个常量ALTERNATIVE_HASHING_THRESHOLD_DEFAULT,它的注释提供了一些值得注意的信息:

/**
 * The default threshold of map capacity above which alternative hashing is
 * used for String keys. Alternative hashing reduces the incidence of
 * collisions due to weak hash code calculation for String keys.
 * <p/>
 * This value may be overridden by defining the system property
 * {@code jdk.map.althashing.threshold}. A property value of {@code 1}
 * forces alternative hashing to be used at all times whereas
 * {@code -1} value ensures that alternative hashing is never used.
 */
 static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

大意是说,ALTERNATIVE_HASHING_THRESHOLD_DEFAULT是一个默认的阈值,当一个键值对的键是String类型时,且map的容量达到了这个阈值,就启用备用哈希(alternative hashing)。备用哈希可以减少String类型的key计算哈希码(更容易)发生哈希碰撞的发生率。该值可以通过定义系统属性jdk.map.althashing.threshold来指定。如果该值是1,表示强制总是使用备用哈希;如果是-1则表示禁用。

HashMap有一个静态内部类Holder,它的作用是在虚拟机启动后根据jdk.map.althashing.threshold和ALTERNATIVE_HASHING_THRESHOLD_DEFAULT初始化ALTERNATIVE_HASHING_THRESHOLD,相关代码如下:

	/**
 * Holder维护着一些只有在虚拟机启动后才能初始化的值
 */
 private static class Holder {
 /**
 * 触发启用备用哈希的哈希表容量阈值
 */
 static final int ALTERNATIVE_HASHING_THRESHOLD;
 static {
 // 读取JVM参数 -Djdk.map.althashing.threshold
 String altThreshold = java.security.AccessController.doPrivileged(
 new sun.security.action.GetPropertyAction(
 "jdk.map.althashing.threshold"));
 int threshold;
 try {
 // 如果该参数没有值,采用默认值
 threshold = (null != altThreshold)
 ? Integer.parseInt(altThreshold)
 : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
 // 如果参数值为-1,禁用备用哈希
 // ALTERNATIVE_HASHING_THRESHOLD_DEFAULT也是等于Integer.MAX_VALUE
 // 所以jdk默认是禁用备用哈希的
 if (threshold == -1) {
 threshold = Integer.MAX_VALUE;
 }
 // 参数为其它负数,则视为非法参数
 if (threshold < 0) {
 throw new IllegalArgumentException("value must be positive integer.");
 }
 } catch(IllegalArgumentException failed) {
 throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
 }
 ALTERNATIVE_HASHING_THRESHOLD = threshold;
 }
 } 

之前提到过,inflateTable中最后还调用了一个initHashSeedAsNeeded(capacity)方法,该方法是用来依据容量决定是否需要初始化hashSeed,hashSeed默认是0,如果初始化hashSeed。所以下面来看看这个方法:

 /**
 * A randomizing value associated with this instance that is applied to
 * hash code of keys to make hash collisions harder to find. If 0 then
 * alternative hashing is disabled.
 */
 transient int hashSeed = 0;
 /**
 * 按需初始化哈希种子
 */
 final boolean initHashSeedAsNeeded(int capacity) {
 // 如果hashSeed != 0,表示当前正在使用备用哈希
 boolean currentAltHashing = hashSeed != 0;
 // 如果vm启动了且map的容量大于阈值,使用备用哈希
 boolean useAltHashing = sun.misc.VM.isBooted() &&
 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
 // 异或操作,如果两值同时为false,或同时为true,都算是false。
 boolean switching = currentAltHashing ^ useAltHashing;
 if (switching) {
 // 把hashSeed设置成随机值
 hashSeed = useAltHashing
 ? sun.misc.Hashing.randomHashSeed(this)
 : 0;
 }
 return switching;
 }

从hashSeed变量的注释可以看出,哈希种子一个随机值,在计算key的哈希码时会用到这个种子,目的是为了进一步减少哈希碰撞。如果hashSeed=0表示禁用备用哈希。

而Holder中维护的ALTERNATIVE_HASHING_THRESHOLD是触发启用备用哈希的阈值,该值表示,如果容器的容量(注意是容量,不是实际大小)达到了该值,容器应该启用备用哈希。

Holder会尝试读取JVM启动时传入的参数-Djdk.map.althashing.threshold并赋值给ALTERNATIVE_HASHING_THRESHOLD。它的值有如下含义:

  • ALTERNATIVE_HASHING_THRESHOLD = 1,总是使用备用哈希
  • ALTERNATIVE_HASHING_THRESHOLD = -1,禁用备用哈希

在initHashSeedAsNeeded(int capacity)方法中,会判断如果容器的容量>=ALTERNATIVE_HASHING_THRESHOLD,就会生成一个随机的哈希种子hashSeed,该种子会在put方法调用过程中的hash方法中使用到:

 /**
 * 获取key的哈希码,并应用一个补充的哈希函数,构成最终的哈希码。
 * This is critical because HashMap uses power-of-two length hash tables, that
 * otherwise encounter collisions for hashCodes that do not differ
 * in lower bits. Note: Null keys always map to hash 0, thus index 0.
 */
 final int hash(Object k) {
 // 如果哈希种子是随机值,使用备用哈希
 // (方法调用链:inflateTable()-->initHashSeedAsNeeded()-->hash(),
 // 在initHashSeedAsNeeded()中已判断了是否需要初始化哈希种子)
 int h = hashSeed;
 if (0 != h && k instanceof String) {
 return sun.misc.Hashing.stringHash32((String) k);
 }
 h ^= k.hashCode();
 // This function ensures that hashCodes that differ only by
 // constant multiples at each bit position have a bounded
 // number of collisions (approximately 8 at default load factor).
 h ^= (h >>> 20) ^ (h >>> 12);
 return h ^ (h >>> 7) ^ (h >>> 4);
 }

计算存储下标(indexFor)

/**
 * 根据哈希码计算返回哈希表的下标
 */
 static int indexFor(int h, int length) {
 // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
 return h & (length-1);
 }

这段代码和简单,却有几个有意思的地方。

为什么容量要设计成2的n次幂

注意,容量实质就是内部数组的length,还要注意是2的n次幂,不是2的倍数。先看下面的测试代码:

public class Main {
 static final int hash(Object k) {
 int hashSeed = 0;
 int h = hashSeed;
 if (0 != h && k instanceof String) {
 return sun.misc.Hashing.stringHash32((String) k);
 }
 h ^= k.hashCode();
 h ^= (h >>> 20) ^ (h >>> 12);
 return h ^ (h >>> 7) ^ (h >>> 4);
 }
 static int indexFor(int h, int length) {
 return h & (length-1);
 }
 public static void main(String[] args) {
 String key = "14587";
 int h = hash(key);
 int capacity = 16;
 for (int i = 0; i < 10; i++) {
 System.out.println(String.format("哈希码: %d, 容量: %d, 下标: %d",
 h, // 同一个哈希码
 (capacity<<i), // 不同的容量
 indexFor(h,capacity<<i))); //计算出来的下标
 }
 }
// key: hello
// 哈希码: 96207088, 容量: 16, 下标: 0
// 哈希码: 96207088, 容量: 32, 下标: 16
// 哈希码: 96207088, 容量: 64, 下标: 48
// 哈希码: 96207088, 容量: 128, 下标: 112
// 哈希码: 96207088, 容量: 256, 下标: 240
// 哈希码: 96207088, 容量: 512, 下标: 240
// 哈希码: 96207088, 容量: 1024, 下标: 240
// 哈希码: 96207088, 容量: 2048, 下标: 240
// 哈希码: 96207088, 容量: 4096, 下标: 240
// 哈希码: 96207088, 容量: 8192, 下标: 240
// key: 4
// 哈希码: 55, 容量: 16, 下标: 7
// 哈希码: 55, 容量: 32, 下标: 23
// 哈希码: 55, 容量: 64, 下标: 55
// 哈希码: 55, 容量: 128, 下标: 55
// 哈希码: 55, 容量: 256, 下标: 55
// 哈希码: 55, 容量: 512, 下标: 55
// 哈希码: 55, 容量: 1024, 下标: 55
// 哈希码: 55, 容量: 2048, 下标: 55
// 哈希码: 55, 容量: 4096, 下标: 55
// 哈希码: 55, 容量: 8192, 下标: 55
// key: 14587
// 哈希码: 48489485, 容量: 16, 下标: 13
// 哈希码: 48489485, 容量: 32, 下标: 13
// 哈希码: 48489485, 容量: 64, 下标: 13
// 哈希码: 48489485, 容量: 128, 下标: 13
// 哈希码: 48489485, 容量: 256, 下标: 13
// 哈希码: 48489485, 容量: 512, 下标: 13
// 哈希码: 48489485, 容量: 1024, 下标: 13
// 哈希码: 48489485, 容量: 2048, 下标: 1037
// 哈希码: 48489485, 容量: 4096, 下标: 1037
// 哈希码: 48489485, 容量: 8192, 下标: 1037
}

上にあるコピーソースコードから上記ハッシュ、indexForのHashMap、hashSeed = 0入れ実行される以下の標準的な方法によりデフォルト値の算出するハッシュマップ、ハッシュコードによって主キー方法と、配列の長さのハッシュコードが計算されますロジック。試験結果から分かるように、複数の拡張、より少ない変動指標indexFor使用しているアルゴリズムは、これはモバイル拡張の操作の数を減らすことができ、同じハッシュコードは、エントリを引き起こしました。

あなたは... 16,32,64の下で、キー4のときindexFor計算を対象プロセスの能力を見てみることができます。

字符串“4”的哈希码是:55(二进制110111)
当length = 16时:
 h & (length-1)
= 55 & (16-1)
= 110111 & 1111
当length = 32时:
 h & (32-1)
= 55 & (16-1)
= 110111 & 11111
当length = 64时:
 h & (length-1)
= 55 & (64-1)
= 110111 & 111111

ここに画像を挿入説明
容量の拡張は、特定の回数(左側に赤の点線)にターンした後、各時間(容量×2)で倍、およびhビット演算で実行されるので、必ず1が全てそうインデックスは同じであろう計算されます。この方法では、拡張は屈折率変化を引き起こすが、比較的安定してますが。

ただ、容量は17,33,65バイナリの場合...その後(左端1)高のほかにな長さ-1が1であり、残りはより簡単に計算したインデックスの操作を行うには0、異なるハッシュと長さ-1である、と思いますインデックスを複製します。な長さ-1のすべてのビットは、計算された標準は、衝突を低減するためにハッシュのより均一な分布を可能にする、1です。

するために、容量が2のn乗に設計されたものの概要:

  • 方法に入れ、それは添字indexFor呼を算出し、容量が2比較的均一な添字のn乗がハッシュ衝突を減らすように設計されています
  • 膨張関連転写法では、インデックスを再計算indexForコールがあります。容量は、2のn乗が拡張インデックスを再計算するときに比較的安定であることを確認移動要素を低減するように設計されています

拡張およびスレッドの安全性の問題

/**
 * Rehashes the contents of this map into a new array with a
 * larger capacity. This method is called automatically when the
 * number of keys in this map reaches its threshold.
 *
 * If current capacity is MAXIMUM_CAPACITY, this method does not
 * resize the map, but sets threshold to Integer.MAX_VALUE.
 * This has the effect of preventing future calls.
 *
 * @param newCapacity the new capacity, MUST be a power of two;
 * must be greater than current capacity unless current
 * capacity is MAXIMUM_CAPACITY (in which case value
 * is irrelevant).
 */
 void resize(int newCapacity) {
 // 缓存就哈希表数据
 Entry[] oldTable = table;
 int oldCapacity = oldTable.length;
 if (oldCapacity == MAXIMUM_CAPACITY) {
 threshold = Integer.MAX_VALUE;
 return;
 }
 // 用扩容容量创建一个新的哈希表
 Entry[] newTable = new Entry[newCapacity];
 transfer(newTable, initHashSeedAsNeeded(newCapacity));
 table = newTable;
 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
 }
 /**
 * 把所有条目从当前哈希表转移到新哈希表
 */
 void transfer(Entry[] newTable, boolean rehash) {
 int newCapacity = newTable.length;
 for (Entry<K,V> e : table) {
 while(null != e) {
 Entry<K,V> next = e.next;
 if (rehash) {
 e.hash = null == e.key ? 0 : hash(e.key);
 }
 int i = indexFor(e.hash, newCapacity);
 e.next = newTable[i];
 newTable[i] = e;
 e = next;
 }
 }
 }

ここに画像を挿入説明
転送リストが逆になり後の表示から図転送処理は、見ることができます。3-> 7>図9は、9-> 7> 3となります。シングルスレッド環境では、閉ループではありません。

しかし、マルチスレッド環境では、複数のスレッドがグローバル変数テーブルにアクセスし、エントリを指し示すインデックスを修正するために転送し、転送メソッドを呼び出しがあるかもしれません。転写工程は逆の順序でリンクされたリストにつながるので、閉ループ基準に発生する可能性がある:3-> 7-> 9-> 3、およびgetメソッドを呼び出すときに、それは無限ループです。

私は助ける、と一緒に進行します。それのようなポイント、確実に!

おすすめ

転載: blog.csdn.net/XingXing_Java/article/details/90669081