# 字段
# table: Entry<K, V>[]
哈希表,数组实现,用于存放元素;每一格是一个哈希桶(链表实现)。
HashMap$Entry<K, V> { | |
key: K; | |
value: V; | |
next: Entry<K, V>; | |
hash: int; | |
} |
# threshold: int
扩容的阈值,如果达到此阈值,则进行扩容;下一扩容容量 * loadFactor。
# loadFactor: float
用于计算扩容阈值(threshold)。
# size: int
元素个数。
# modCount: int
- modifyCount 修改次数,只会累加;执行修改操作影响到 key 才会 + 1,如:put, remove, clear;如果只是修改 value,则 modCount 不会变化。
- 目的是为了迭代器遍历时判断集合的结构(key 变化)是否被修改。
a. 初始化迭代器时会保存此值,每次迭代会比较集合的 modCount 和迭代器的 modCount 是否一致
如果发生变化,就认为集合被修改了,然后立即抛出异常 ConcurrentModificationException
# hashSeed: int
用于生成 hash,0 或 随机值(为了降低 hash 碰撞)
# 构造函数

- 检查入参 :initialCapacity [初始容量]、loadFactor [负载因子]:
a. initialCapacity 的范围
b. loadFactor 大于 0,且非 NaN - 赋值 threshold,loadFactor:
a. 默认 threshold = 16, loadFactor = 0.75 - init 空函数钩子:
a. 初始化集合后可自定义额外功能;子类继承可以实现此方法 - table 是懒初始化
# 初始化 table
按需初始化,插入元素时才开始初始化 table。

- 插入元素,先判断 table 是否为空表;如果是,执行初始化 table 操作
# inflateTable

- 将初始化的大小转换为 2 的 n 次幂形式,但不超过本身值,转换后作为容量值
- 设置阈值 threshold = 容量值 * loadFactor;不能超过最大值 (1 << 30) + 1
- 创建 table 数组,容量值为第 1 步计算出来的值
- 按需初始化 hashSeed,后续根据 key 计算 hash 时需要
# roundUpToPowerOf2

- 判断入参的范围 的边界值,超出范围则设置为边界值
- 非边界值,且在范围 内,则转换为:最大的 2 的幂数,且不大于本身
如:
16 --> 16
15 --> 8 - 2 的幂数:取整数的二进制形式的最高位即可 Integer.highestOneBit ()
# highestOneBit

- 如何将低位全部置为 1
a. 32 位整数:5 次 [右移 + 相加] 即可,右移分别为:1, 2, 4, 8, 16![将低位全部置为 1 binary-set-all-to-1]()
- 获取整数的二进制形式的最高位 1
a. 低位全部置为 1
b. 无符号右移 1 位
c. 相减![二进制的最高位 highestOneBit]()
# initHashSeedAsNeeded

一、此函数的目的是设置 hashSeed 的值
- 容量达到一定数量后,hash 碰撞机率会逐渐增大,因此到达一定阈值,需要使用随机 hashSeed
- 扩容容量低于替换阈值,使用 0;扩容容量高于替换阈值,使用随机值(降低 hash 碰撞)
a. 第一次:hashSeed 默认值 0,容量 [未达到] 替换阈值,switching = falseb. 第二次:hashSeed 默认值 0,容量 [达到] 替换阈值,switching = true,生成随机 hashSeedswitching = currentAltHashing ^ useAltHashing;
// false = false ^ falsec. 第三次:hashSeed 是随机值,容量 [达到] 替换阈值,switching = false;已经是随机值,不需要再次生成switching = currentAltHashing ^ useAltHashing;
// true = false ^ trued. 第四次:hashSeed 是随机值,容量 [未达到] 替换阈值,switching = true;容量已经下降到阈值以下,hashSeed 随机值重置为 0switching = currentAltHashing ^ useAltHashing;
// false = true ^ trueswitching = currentAltHashing ^ useAltHashing;
// true = true ^ false
二、控制 hashSeed 的参数
- currentAltHashing:当前 hashSeed 是否为随机值
- useAltHashing:扩容容量是否超出替换阈值,且 VM 是否已启动
- switching:hashSeed 需要替换;由 currentAltHashing ^ useAltHashing 决定
三、随机值生成:ThreadLocalRandom.current ().nextInt ()
四、替换阈值,在 HashMap$Holder 中持有
初始化 Holder.ALTERNATIVE_HASHING_THRESHOLD 规则;根据 jdk.map.althashing.threshold 配置来初始化静态成员变量:
- 不配置(默认)或 -1:阈值设置为 Integer.MAX_VALUE 值,表示不启用随机 hashSeed。
a. 由于阈值是 Integer.MAX_VALUE,此阈值是条件无法达到的,因此表示关闭。
b. 不启用随机的 hashSeed,但 hashSeed 默认值为 0,使用 0 作为 seed。 - 小于 0:抛出 Error 异常。
- >= 0:当容量 >= 阈值时,开启随机 hashSeed。
Holder 的作用是为了懒加载,用到此类才加载此类;静态代码块在加载类期间执行,jvm 会保证线程安全。
# 添加 null key 元素

- 插入元素,如果 key 为 null,则执行 putForNullKey 操作。
# putForNullKey

key 为 null 的元素,存放到 table [0] 下面:
- 先判断 null key 元素在 table [0] 的链表上是否已经存在,for 循环遍历 Entry 链表。
- null key 存在于链表上:for 循环定位到具体 Entry,则修改 Entry 的值,然后返回旧值。
a. recordAccess 是钩子函数,在 HashMap 是空方法;LinkedHashMap 中实现了重排序。
b. recordAccess 只有在 put 方法中覆盖 value 时(key 已经存在)才调用;可以实现扩展功能,如 LinkedHashMap 的 accessOrder 属性。 - null key 不存在于链表上:modCount++ 然后在 table [0] 下面添加新 Entry,返回旧值 null(新增没有旧值)。
![addEntry addEntry]()
a. 如果元素超过扩容阈值,且当前数组下标下链表为空(当前桶为空);则进行扩容
b. 在此数组下标下的链表添加元素,size++:头插法![createEntry createEntry]()
# 添加非 null key 元素

- 计算 key 的 hash 值。
- 计算 hash 值对应的桶下标:h & (length-1);确保 length 是 2 的 n 次幂,则等价于 h % length。
- 得到桶在数组里的下标后,遍历桶元素,判断此 key 是否已经存在于桶(链表)中。
a. 如果存在于桶中:hash, key 要一致;在遍历过程中得到 Entry,更新 Entry 的值,然后返回旧值。
b. 不存在于桶中:modCount++ 然后在桶中添加新元素:头插法。
# hash 计算

如果 hashSeed 不等于 0,且是字符串,则使用 sun.misc 的工具类计算。
- hashSeed 异或 key 的 hashCode。
- 扰动函数 / 计算,高位和低位进行异或,增加随机性,为了降低 hash 的冲突。
# 获取 key 元素

- 判断 key 是否为 null,如果为 null,则调 getForNullKey 函数。
![getForNullKey getForNullKey]()
a. 判断 size 是否有元素。
b. 从数组第 0 个桶里遍历,是否存在 key 为 null 的元素,然后返回 Entry 的值;找不到返回 null。 - 如果 key 不为 null,调 getEntry 获取 Entry。
![getEntry getEntry]()
a. 判断 size 是否有元素。
b. 计算 key 的 hash 值,计算 hash 对应 table 数组的下标,即桶的位置。
c. 遍历桶里是否存在 hash, key 都相等的元素,然后返回 Entry;找不到就返回 null。 - 返回 entry.value,需要进行空指针判断。
# 移除 key 元素

移除元素,然后取得元素对应的 Entry,最后返回 Entry 的值

# 扩容
put() -> addEntry()
元素个数达到扩容阈值,且当前桶元素不为空,才执行扩容;新扩容容量是原容量的 2 倍。

# resize

- 如果原 table 的容量已经是最大值,则设置扩容的阈值为 Integer.MAX_VALUE;然后直接退出。
- 创建新表 newTable,容量是原容量的 2 倍。
- 旧表的数据迁移到新表,以及按需重新计算全部元素的 hash(hashSeed 变化则需要重新计算 hash)。
![transfer transfer]()
a. 遍历旧表所有桶;遍历桶里的所有元素。
b. 先记录当前处理的结点的后继结点(下一轮循环需要),否则 e.next 在迁移结点过程中被覆盖。
c. 如果需要重新计算 hash(入参 rehash),则重新计算当前结点元素 key 的 hash 值。
d. 根据 hash 值和新表的容量,计算新表的下标;然后头插法将当前结点迁移到新表中。
e. 继续下一轮循环:e == null 则表示到达链表的尾部,终止链表循环,进行下一个桶的循环。 - 新表替换旧表。
- 重新计算下次扩容的阈值,扩容阈值 = 新容量 * 负载因子;不能超出最大容量 + 1。
# 死链的原因
- 多线程扩容后,都执行完 next = e.next
- 线程 2 的 CPU 时间片用完而暂停,线程 1 继续执行迁移代码
- 线程 1 执行完成后,线程 2 继续运行,执行迁移代码,导致链表出现环状
- 调用 hashmap.get (key) 后,如果访问到环,且 key 不存在,则导致死循环
# 死链的复现
# 设置断点
JDK 1.7 版本代码,设置断点:

- 在 next = e.next 的下一行代码设置条件断点,即 if (rehash) 这里
a. 条件断点:"t1,t2".contains (Thread.currentThread ().getName ())
b. 使用 contains 或 equals 来比较,不要用 == 比较;字符串对象可能内存不同 - 等到 t1, t2 线程都停在此处后,删除断点;然后依次执行完 t1, t2 线程的代码,这样就构造出环状的链表了。
# 构造模拟代码
// 在 HashMap 的 transfer 代码里的 if (rehash) 这条语句里 | |
// 添加条件断点:"t1,t2".contains (Thread.currentThread ().getName ()) | |
// 添加普通断点也是可以的,这里为了防止其它线程干扰 | |
// 在启动 main 方法前设置断点,debug 模式运行 | |
public static void main(String[] args) throws InterruptedException { | |
// 初始化容量为 16,扩容阈值 = 16 * 0.75 = 12,即超过 12 个就触发扩容 | |
final HashMap<Integer, Integer> map = new HashMap<>(); | |
// 填充 10 个元素 | |
for (int i = 2; i < 12; i++) { | |
map.put(i, i); | |
} | |
// 构造特定的 key,让它们都在同一个桶下面,还要考虑扩容后的下标也是一致的 | |
// 以下 key 计算出来的下标都一样(容量为 16 且 32 时): | |
// 1, 35, 69, 103, 136, 170 | |
map.put(1, 1); | |
map.put(35, 35); | |
// 以上插入了 12 个元素,1 和 35 都在下标为 1 的桶里 | |
// 触发扩容条件:哈希表里元素个数达到扩容阈值,且新插入的元素的桶里有元素, | |
// 这里下标 1 的桶里有 1, 35 | |
// 线程 1, 线程 2 同时触发扩容 | |
Thread t1 = new Thread(new Runnable() { | |
public void run() { // 1.7 不支持 lambda 表达式 | |
map.put(56, 56); | |
} | |
}, "t1"); | |
Thread t2 = new Thread(new Runnable() { | |
public void run() { | |
map.put(56, 56); | |
} | |
}, "t2"); | |
t1.start(); t2.start(); | |
t1.join(); t2.join(); | |
//t1, t2 同时进入断点后,先放开 t1 线程的断点,让 t1 跑完 | |
// 然后再放开 t2 线程的断点,让 t2 跑完 | |
// 此时,下标为 1 的桶,形成了环状链表 | |
// 导致死循环:69 的下标也是 1,但这个位置的桶已经形成环状链表 | |
// 但里面没有此元素,则死循环地查找 | |
map.get(69); | |
} |
- 10 个任意的元素 + 2 个特殊的元素(在同一个桶里) = 12
- 插入第 13 个元素,且此元素的桶里有元素(上面构造 2 个特殊元素),才能触发扩容
- 以下 key 是在容量 16 或 32 的时候,桶下标一致的:
1, 35, 69, 103, 136, 170






