Files
JavaYouth/docs/Java/collection/HashMap-JDK7源码讲解.md

1147 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: HashMap-JDK7源码讲解
tags:
- Java集合
- HashMap
categories:
- Java集合
- HashMap
keywords: Java集合HashMap。
description: HashMap-JDK7源码讲解。
cover: 'https://unpkg.zhimg.com/youthlql@1.0.8/Java_Basis/logo.png'
abbrlink: f1f58db2
date: 2020-11-01 10:21:58
---
> 本文基于版本 `JDK 1.7`,即 `Java 7`。
# HashMap用法
## 常用API
```java
V get(Object key); // 获得指定键的值
V put(K key, V value); // 添加键值对
void putAll(Map<? extends K, ? extends V> m); // 将指定Map中的键值对 复制到 此Map中
V remove(Object key); // 删除该键值对
boolean containsKey(Object key); // 判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value); // 判断是否存在该值的键值对;是 则返回true
Set<K> keySet(); // 单独抽取key序列将所有key生成一个Set
Collection<V> values(); // 单独value序列将所有value生成一个Collection
void clear(); // 清除哈希表中的所有键值对
int size(); // 返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty(); // 判断HashMap是否为空size == 0时 表示为 空
```
```java
import java.util.*;
import java.util.concurrent.*;
/**
* @Author: youthlql-吕
* @Date: 2020/10/11 21:05
* <p>
* 功能描述:
*/
public class test {
//声明1个 HashMap的对象
static Map<String, Integer> map = new HashMap<>();
public static void main(String[] args) {
//向HashMap添加数据成对 放入 键 - 值对)
map.put("Java", 1);
map.put("hadoop", 2);
map.put("产品经理", 3);
//获取 HashMap 的某个数据
System.out.println("key = 产品经理时的值为:" + map.get("产品经理"));
System.out.println();
System.out.println("------------------下面是遍历-------------------");
test();
}
/**
* 获取HashMap的全部数据遍历HashMap
*/
public static void test(){
// 方法1获得key-value的Set集合 再遍历
System.out.println("------------方法1------------");
// 1、获得key-value对Entry的Set集合
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
// 2、遍历Set集合从而获取key-value
for (Map.Entry<String, Integer> entry : entrySet) {
System.out.print(entry.getKey());
System.out.println(entry.getValue());
}
System.out.println("----------");
// 2.1、通过迭代器先获得key-value对Entry的Iterator再循环遍历
Iterator iter1 = entrySet.iterator();
while (iter1.hasNext()) {
// 遍历时需先获取entry再分别获取key、value
Map.Entry entry = (Map.Entry) iter1.next();
System.out.print((String) entry.getKey());
System.out.println((Integer) entry.getValue());
}
// 方法2获得key的Set集合 再遍历
System.out.println("------------方法2------------");
// 1. 获得key的Set集合
Set<String> keySet = map.keySet();
// 2. 遍历Set集合从而获取key再获取value
// 2.1 通过for循环
for (String key : keySet) {
System.out.print(key);
System.out.println(map.get(key));
}
System.out.println("----------");
// 2.2 通过迭代器先获得key的Iterator再循环遍历
Iterator iter2 = keySet.iterator();
String key = null;
while (iter2.hasNext()) {
key = (String) iter2.next();
System.out.print(key);
System.out.println(map.get(key));
}
// 方法3获得value的Set集合 再遍历
System.out.println("------------方法3------------");
// 1. 获得value的Set集合
Collection valueSet = map.values();
// 2. 遍历Set集合从而获取value
// 2.1 获得values 的Iterator
Iterator iter3 = valueSet.iterator();
// 2.2 通过遍历直接获取value
while (iter3.hasNext()) {
System.out.println(iter3.next());
}
}
}
/**
* 对于遍历方式,推荐使用针对 key-value对Entry的方式效率高
* 原因:
* 1. 对于 遍历keySet 、valueSet实质上 = 遍历了2次
* 第一次转为iterator迭代器遍历、
* 第二次从HashMap中取出key和value操作
* 2. 对于遍历entrySet实质只遍历了1次也就是获取存储实体Entry存储了key 和 value
*/
```
**结果**
```
key = 产品经理时的值为3
------------------下面是遍历-------------------
------------方法1------------
Java1
hadoop2
产品经理3
----------
Java1
hadoop2
产品经理3
------------方法2------------
Java1
hadoop2
产品经理3
----------
Java1
hadoop2
产品经理3
------------方法3------------
1
2
3
```
# 数据结构
## 简介
1、HashMap是散列表的一种HashMap本身采用数组来进行储存。同时HashMap采用拉链法来解决hash冲突拉链法就是通过链表来解决hash冲突。所以说整体上来看HashMap采用的数据结构 = **数组(主) + 单链表(副)**
大致是这样的一个结构
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0001.png">
- 每个链表就算哈希表的桶bucket
- 链表的节点值就算一个键值对
## 重要参数介绍
**构造函数源码**
先贴一下,后面参数介绍会使用到。
```java
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();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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);
}
```
> 下面的代码有些顺序和源码可能不太一样,仅仅是顺序,笔者是为了更好的阅读体验
```java
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
/*
名词介绍:
1、容量capacity HashMap中数组【强调一下是数组不是元素个数】的长度
2、容量范围必须是2的幂并且小于最大容量2的30次方
3、初始容量 = 哈希表创建时的容量
*/
//默认初始容量 = 哈希表创建时的容量。默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 十进制的2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量 = 2的30次方若传入的容量过大将被最大值替换
static final int MAXIMUM_CAPACITY = 1 << 30;
/*
1、加载因子(Load factor)HashMap在其容量自动增加前的一种尺度。
2、加载因子越大、填满的元素越多 = 空间利用率高、但hash冲突的机会加大、查找效率变低因为链表变长了
3、加载因子越小、填满的元素越少 = 空间利用率小、hash冲突的机会减小、查找效率高链表不长
*/
// 实际加载因子
final float loadFactor;
// 默认加载因子 = 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//就是上面说的数组hashmap用Entry数组储存k-v键值对
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static final Entry<?,?>[] EMPTY_TABLE = {};
// HashMap的大小即HashMap中存储的键值对的数量。注意和容量区分开容量是数组Entry的长度
transient int size;
/*
1、扩容阈值threshold当哈希表的大小【就是上面的size】 ≥ 扩容阈值时,就会扩容哈希表
即扩充HashMap的容量
2、扩容 = 对哈希表进行resize操作即重建内部数据结构从而哈希表将具有大约两倍的桶数
3、扩容阈值 = 容量 x 加载因子
*/
int threshold;
```
## 数组-Entry
```java
/**
* Entry类实现了Map.Entry接口
* 即 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法
**/
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; // 键
V value; // 值
Entry<K,V> next; // 指向下一个节点 也是一个Entry对象从而形成解决hash冲突的单链表
int hash; // hash值
/**
* 构造方法创建一个Entry
* 参数哈希值h键值k值v、下一个节点n
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
// 返回 与 此项 对应的键
public final K getKey() {
return key;
}
// 返回 与 此项 对应的值
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
/**
* equals
* 作用判断2个Entry是否相等必须key和value都相等才返回true
*/
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
/**
* hashCode
*/
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* 当向HashMap中添加元素时即调用put(k,v)时,
* 对已经在HashMap中k位置进行v的覆盖时会调用此方法
* 此处没做任何处理
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* 当从HashMap中删除了一个Entry时会调用该函数
* 此处没做任何处理
*/
void recordRemoval(HashMap<K,V> m) {
}
}
```
`HashMap`中的数组元素 & 链表节点 采用 `Entry`类实现
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0001.png">
1、一个正方形代表一个Entry对象同时也代表一个键值对。
2、即 `HashMap`的本质 = 1个存储`Entry`类对象的数组 + 多个单链表
3、Entry对象本质 = 1个映射键 - 值对),属性包括:键(`key`)、值(`value`)、 下个节点( `next`) = 单链表的指针 = 也是一个`Entry`对象,用于解决`hash`冲突
# 构造函数源码
```java
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable{
// 省略上节阐述的参数
/*
构造函数1默认构造函数无参
加载因子和容量为默认分别是0.75和16
*/
public HashMap() {
/*
实际上是调用构造函数3指定"容量大小"和"加载因子"的构造函数
传入的指定容量和加载因子均为默认
*/
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/*
构造函数2指定"容量大小"的构造函数
加载因子是默认的0.75 、容量为指定大小
*/
public HashMap(int initialCapacity) {
// 实际上是调用的也是构造函数3只是在传入的加载因子参数为默认加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/*
构造函数3指定“容量大小”和“加载因子”的构造函数
加载因子和容量都是程序员自己指定
*/
public HashMap(int initialCapacity, float loadFactor) {
// HashMap的最大容量只能是MAXIMUM_CAPACITY哪怕传入的 > 最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 设置加载因子
this.loadFactor = loadFactor;
/*
设置扩容阈值 = 初始容量
1、注意此处不是真正的阈值仅是为了接收参数初始容量大小capacity、加载因子(Load factor)
并没有真正初始化哈希表即初始化存储数组table
2、真正初始化哈希表初始化存储数组table是在第1次添加键值对时即第1次调用put()时,下面会
详细说明。
*/
threshold = initialCapacity;
init(); // 一个空方法用于未来的子对象扩展
}
/*
构造函数4包含“子Map”的构造函数
即构造出来的HashMap包含传入Map参数
加载因子和容量均为默认
*/
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);
// 将传入的子Map中的全部元素逐个添加到HashMap中
putAllForCreate(m);
}
}
```
# put()源码
## put()
```java
public V put(K key, V value) {
/* ①
1、若哈希表未初始化即 table为空)则调用inflateTable方法使用构造函数时设置的阈值
(即初始容量)初始化数组table
*/
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
/* ②
1、判断key是否为空值null
2、若key == null则调用putForNullKey方法putForNullKey方法最终将该键-值存放到数组
table中的第1个位置即table[0]。本质key = Null时hash值 = 0故存放到table[0]中)
该位置永远只有1个value新传进来的value会覆盖旧的value
3、k != null往下走
*/
if (key == null)
return putForNullKey(value);
//下面就是k != null的情况
//计算key的hash值
int hash = hash(key);
//根据hash值最终获得key对应存放的数组Table中位置(也就是数组下标)
int i = indexFor(hash, table.length);
/* ③
1、通过遍历以该数组元素为头结点的链表逐个判断是否发生hash冲突同时判断该key对应的值是
否已存在
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
/* ④
1、如果发生了hash冲突且key也相等。则用新value替换旧value(此时说明发生了更新的情况)
注意这里强调的是发生了hash冲突并且key也相等。
*/
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//修改次数+1
modCount++;
/* ⑤
1、走到这一步有两种情况
1-1、没有发生hash冲突
1-2、发生了hash冲突但是没有在链表中找到key一样的键
2、接着准备采用头插法插入链表的头部(也就是数组里面)
*/
addEntry(hash, key, value, i);
return null;
}
```
## inflateTable()
```java
private void inflateTable(int toSize) {
/*
将传入的容量大小转化为:>传入容量大小的最小的2的次幂即如果传入的是容量大小是18那么转化后
初始化容量大小为32即2的5次幂
*/
int capacity = roundUpToPowerOf2(toSize);
//重新计算阈值 threshold = 容量 * 加载因子
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
/*
使用计算后的初始容量已经是2的次幂 初始化数组table作为数组长度即 哈希表的容量大小 =
数组大小(长度)
*/
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
```
### roundUpToPowerOf2()
```java
private static int roundUpToPowerOf2(int number) {
//若容量超过了最大值,初始化容量设置为最大值 ;否则,设置为>传入容量大小的最小的2的次幂
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
```
## putForNullKey()
当 key ==null时将该 key-value 的存储位置规定为数组table 中的第1个位置即table [0]
```java
private V putForNullKey(V value) {
/*
1、遍历以table[0]为首的链表寻找是否存在key==null对应的键值对
1-1、若有则用新value替换旧value同时返回旧的value值。
*/
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
/*
1、若无key==null的键那么调用addEntry将空键和对应的值封装到Entry中并放到table[0]中
2、能走到这里的情况就是table[0]位置之前没有元素这一次是第一次添加key为null的元素
3、k == null已经是固定放在了table[0]的位置,不会有链表存在,只会有一个元素。
*/
addEntry(0, null, value, 0);
return null;
}
```
从此处可以看出:
* `HashMap`的键`key` 可为`null`(区别于 `HashTable``key` 不可为`null`
* `HashMap`的键`key` 可为`null`且只能为1个但值`value`可为null且为多个
## hash()
> 这个方法比较重要1.7和1.8改动的比较大
```java
/**
* 1、该函数在JDK7和8中的实现不同但原理一样 = 扰动函数 = 使得根据key生成的
* 哈希码hash值分布更加均匀、更具备随机性避免出现hash值冲突即指不同key但生成同1个hash值
* 2、JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
* 3、JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
*/
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
//4次位运算 + 5次异或运算
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
```
## indexFor()
```java
//这里h & (length-1)的意思就是hash值与数组长度取模。只是因为数组长度是特殊的2的幂
//所以这个等价关系刚好成立
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);
}
```
## addEntry()
```java
//作用添加键值对Entry到HashMap中。参数3:插入数组table的索引位置,也就是数组下标
//注意JDK7是先扩容再插入
void addEntry(int hash, K key, V value, int bucketIndex) {
/*
1、插入前先判断容量是否足够
1.1、若不足够则进行扩容2倍、重新计算Hash值、重新计算存储数组下标
*/
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 1.2、若容量足够则创建1个新的数组元素Entry 并放入到数组中
createEntry(hash, key, value, bucketIndex);
}
```
# 扩容源码
下面的也是JDK7扩容的步骤接着上面的`addEntry()`
## resize()
```java
/**
* 作用当大小不足时size > 阈值注意是size不是容量则扩容扩到2倍
*/
void resize(int newCapacity) {
// 1、保存旧数组old table
Entry[] oldTable = table;
// 2、保存旧容量old capacity ),即数组长度
int oldCapacity = oldTable.length;
// 3、若旧容量已经是系统默认最大容量了那么将阈值设置成整型的最大值返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 4、根据新容量2倍容量新建1个数组即newTable
Entry[] newTable = new Entry[newCapacity];
//5、将旧数组上的数据键值对转移到新table中从而完成扩容
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 6、新数组table引用到HashMap的table属性上
table = newTable;
// 7、重新设置阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
```
## transfer()
```java
/**
* 作用将旧数组上的数据键值对转移到新table中从而完成扩容
* 过程:按旧链表的正序遍历链表、在新链表的头部依次插入。但是这样会导致扩容完成后,链表逆序
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
for (Entry<K,V> e : table) {
while(null != e) {
/*
1、遍历以该数组元素为首的链表
2、转移链表时因是单链表故要保存下1个结点否则转移后链表会断开
*/
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;
// 访问下1个Entry链上的元素如此不断循环直到遍历完该链表上的所有节点
e = next;
}
}
}
```
大概画了一下图:
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0002.png"/>
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0003.png">
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0004.png">
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0005.png">
1、在扩容`resize`过程中,在将旧数组上的数据转移到新数组上时,转移操作就是按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况。
2、设重新计算存储位置后不变即扩容前 1->2->3扩容后 = 3->2->1
3、此时若多线程并发执行 put操作一旦出现扩容情况**容易出现环形链表**从而在获取数据、遍历链表时形成死循环Infinite Loop即**线程不安全**。
## createEntry()
```java
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++; //这里才进行size++
}
```
举个例子说明流程:
1、假设hashmap中容量为16加载因为为0.75 = 12。
2、此时hashmap中有11个元素也就是size == 11在你添加第12个元素时。看代码此时size还是11所以并不会扩容。只有在你调用完`createEntry()`,`size++`执行完毕后size变成12。
3、在添加第13个元素时才会进入if逻辑里进行先扩容。
```java
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);
}
```
# 扩容出现的死循环链表
```java
/**
* 作用将旧数组上的数据键值对转移到新table中从而完成扩容
* 过程:按旧链表的正序遍历链表、在新链表的头部依次插入。但是这样会导致扩容完成后,链表逆序
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
for (Entry<K,V> e : table) {
while(null != e) {
/*
1、遍历以该数组元素为首的链表
2、转移链表时因是单链表故要保存下1个结点否则转移后链表会断开
*/
Entry<K,V> next = e.next; //pos_1
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//重新计算每个元素的存储位置
int i = indexFor(e.hash, newCapacity);
//这个地方暂时先放着,后面讲死循环链表的时候会讲到
e.next = newTable[i];
//讲当前元素,赋给新数组的对应下标位置。
newTable[i] = e;
// 访问下1个Entry链上的元素如此不断循环直到遍历完该链表上的所有节点
e = next;
}
}
}
```
## 前置条件
1、为了演示方便初始状态时hashmap容量为2加载因子为默认的0.75.
## 步骤1
**hashmap初始状态**
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0006.png">
1、此时只有一个元素扩容阈值为2*0.75 = 1.5。
2、此时假设有两个线程线程a和线程b同时put并且都没有进入到addEntry()方法里的if逻辑【因为此时size都没有++size == 1 1 < 1.5 所以if判断不成立。】。两个线程都准备同时调用createEntry()方法。
```java
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);
}
```
3、线程a put的是 e3 = <k3,v3>。线程b put的是e2 = <k2,v2>。两个都调用了`createEntry()`方法。
## 步骤2
**两个线程调用完毕之后hashmap目前是这样的。**
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0007.png">
此时size==3。下次再进行put的时候**addEntry()方法里的if判断就会成立**。
## 步骤3
1、接着又来了两个线程线程1和线程2。【假设线程1put的是e1线程2put的是e0。其实也不用管它们两put的是谁】
2、两个线程都同时调用resize()方法新数组已经扩容完毕准备转移旧数组上的数据到新数组里。也就是准备调用resize()里的下面这个方法。
```
//5、将旧数组上的数据键值对转移到新table中从而完成扩容
transfer(newTable, initHashSeedAsNeeded(newCapacity));
```
3、来看下此时内存里的状态
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0008.png"/>
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0009.png">
## 步骤4
来看下源码【上面源码里有注释,这里把注释去掉】
```java
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//pos_1
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//pos_2
e.next = newTable[i];
//pos_3
newTable[i] = e;
//pos_4
e = next;
}
}
}
```
1、假设线程1**执行完**代码pos_1位置后暂时挂起。此时e == e2 e.next == e3
2、线程2直接**扩容完毕**那么完成后的状态是这样【假设e2和e3还是hash到同一个位置】
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0010.png">
3、线程1还是原来的状态
> 强调一点线程2已经扩容完毕
## 步骤5
目前两个线程里的新数组是这样的
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0011.png">
为了方便后面观看,我画成这样。
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0012.png">
## 步骤6
```java
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//pos_1
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//pos_2
e.next = newTable[i];
//pos_3
newTable[i] = e;
//pos_4
e = next;
}
}
}
```
> 之前说过假设线程1**执行完**代码pos_1位置后暂时挂起。此时e == e2 e.next == e3【也就是next == e3】
1、线程1唤醒后继续执行pos_2pos_3pos_4
2、执行pos_2意思是e2的next指针指向了线程1的新hash表【也就是newTable1】因为newTable1是新的所以为null
所以e2.next = null。
3、执行pos_3newTable1[3] = e2;
4、执行pos_4 e = e3;
也就变成了下面这个样子。
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0013.png">
## 步骤7
1、线程1继续执行循环
> 注意之前强调过线程2已经扩容完毕那么table就已经被指向了newTable2也就是说第二次循环时线程1所循环的table变量就是newTable2
2、
```java
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//pos_1
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//pos_2
e.next = newTable[i];
//pos_3
newTable[i] = e;
//pos_4
e = next;
}
}
}
```
1、执行pos_1此时e == e3那么next就是 e3.next此时next == e2
2、执行pos_2经过第一轮循环newTable1[3] == e2。那么执行完这行代码后e3.next还是等于e2【相当于没执行】
3、执行pos_3newTable1[3] == e3。
4、执行pos_4e = e2
执行完,变成这样。
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0014.png">
## 步骤8
线程1执行第三次循环
1、执行pos_1next = e2.next得到 next == null。
2、执行pos_2 `e.next = newTable[i]` e2.next == newTable1[3]。也就是相当于 e2.next == e3
3、执行pos_3 newTable[i] = e得到 newTable1[3] == e2
<img src="https://unpkg.zhimg.com/youthlql@1.0.8/Java_collection/HashMap/JDK7/0015.png">
这样就形成了循环链表再get()数据就会陷入死循环。
# get()源码
```java
public V get(Object key) {
//当key==null时则到table[0]为头结点的链表去寻找对应 key == null的键
if (key == null)
return getForNullKey();
//当key ≠ null时去获得对应值
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
if (size == 0) {
return null;
}
// 遍历以table[0]为头结点的链表,寻找 key==null 对应的值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
// 从table[0]中取key==null的value值
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//根据key值通过hash计算出对应的hash值
int hash = (key == null) ? 0 : hash(key);
//根据hash值计算出对应的数组下标,遍历以该数组下标的数组元素为头结点的链表所有节点,
//寻找该key对应的值
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
// 若 hash值和key 相等则证明该Entry = 我们要的键值对
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
```