HashMap
数据结构
数组+链表+(红黑树jdk>=8)
源码原理分析
HashMap属性
//初始容量为16;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转化为红黑树的阈值;
static final int TREEIFY_THRESHOLD = 8;
//红黑树退化为链表的阈值;
static final int UNTREEIFY_THRESHOLD = 6;
//链表转红黑树时,hash表的长度阈值,小于64,则优先扩容;
static final int MIN_TREEIFY_CAPACITY = 64;
//存储数据的Node数组,每个Node是一个链表;
transient Node<K,V>[] table;
//Entry的集合,遍历hash表可能会用到;
transient Set<Map.Entry<K,V>> entrySet;
//哈希表的长度;
transient int size;
final float loadFactor;
HashMap内部类Node
真正存储key-value的结构;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
//指向下一个结点。
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
put()
put流程:
- 判断容器是否为空、如果为空,则进行扩容;
- 对key进行hash运算,再进行与运算
- 判断数组中该位置是否为空,为空,则插入;
- 判断数组该位置的首节点是否和key相等,则覆盖
- 判断数组改为的首页点是否为树结点,则插入红黑树里
- 以上都不成立,则
A:遍历该位置的链表,如果存在key相同结点,则覆盖
B:如果不存在相同的结点,则插入链表的尾部;
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//这里tab=table获得存储数据Node数组;判断是否为空,第一次添加元素则进行扩容;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n-1)&hash求出 在具体在数组中的下标,若该数组元素为空,则直接参加。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//上面的p=tab[(n-1)&hash], p.hash=hash;
//这里的p可能是一条链表的头结点,也可能是一颗红黑树的头结点
//比较已经存在的数组元素的哈希值是否等于新元素的哈希值
//且 判断p.key是否等于新元素的key, 或者 key是否和k相同
//相同则 将存在的p节点传给新节点e;
//判断第一个结点的key是否和新元素的key相等》。。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果p.hash不等于hash,且key也不相等、首元素也是TreeNode,
//该Node元素链表的长度已经超过了8,节点已经变为了树节点。添加红黑树节点。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//遍历这个Node链表;
for (int binCount = 0; ; ++binCount) {
//e=p.next:遍历链表
if ((e = p.next) == null) {
//如果整条链表没有一个结点和新元素相同,则在链表尾部挂上一个新节点。
p.next = newNode(hash, key, value, null);
//如果链表的结点数大于8时,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果链表中某一个结点的哈希值等于新元素的哈希值
//且 key相同。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//这里跳到条下一个结点,顺序遍历。
p = e;
}
}
//如果链表中存在和新元素 key相同的结点,则e不为空。
if (e != null) { // existing mapping for key
//获得旧的value值。
V oldValue = e.value;
//为空则复制。
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回为非空。oldValue;
return oldValue;
}
}
//长度+1,判断是否到了扩容的阈值,到了则进行扩容
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
//返回空。
return null;
}
HashMap的负载因子为什么0.75
loadFactor大于0,且不能是无穷大,默认情况下是0.75;而负载因子的设置很重要,hashMap通过initailCapaciry * loadFactor的乘积来决定当容量到达阈值后进行扩容。
- 如果loadFactor的值太小,会导致hashMap扩容的频率变大且hashMap中的元素利用率低,
- 如果loadFactor的太大,则会导致在hashMap的每个元素利用率增大,但是发生碰撞的概率也升高了。
HashMap与HashTable的区别
- 前者线程不安全,后者线程安全;
- 前者可以存储key为null的数据,后者不能;
- 前者有containsvalue和containsKey方法,后者有contains方法方法
- 前者继承Map,后者继承Directory
JDK1.7与1.8中,HashMap的区别
- JDK1.7使用头插法,JDK1.8使用尾插法
- JDK1.7存储数据的是Entry结点,JDK1.8存储的是Node结点。
- JDK1.7创建对象就会初始化容器,JDK1.8在第一次put操作才初始化容器;
- JDK1.7使用数组+链表,JDK1.8使用数组+链表+红黑树
JDK1.7扩容死锁分析
死锁问题核心在于下面代码,多线程扩容导致形成的链表环!
- 记录oldHash表中e.next;
- rehash计算出数组的位置(hash表中桶的位置)
- e要插入链表的头部, 所以要先将e.next指向new hash表中的第一个 元素
- 将e放入到new hash表的头部
- 转移e到下一个节点, 继续循环下去
单线程扩容
假设:hash算法就是简单的key与length(数组长度)求余。hash表长度为2,如果不扩 容, 那么元素key为3,5,7按照计算(key%table.length)的话都应该碰撞到table[1]上。
扩容:hash表长度会扩容为4重新hash,key=3 会落到table[3]上(3%4=3), 当前 e.next为key(7), 继续while循环重新hash,key=7 会落到table[3]上(7%4=3), 产生碰撞, 这里采用的是头插入法,所以key=7的Entry会排在key=3前面(这里可以具体看while语句 中代码)当前e.next为key(5), 继续while循环重新hash,key=5 会落到table[1]上 (5%4=3), 当前e.next为null, 跳出while循环,resize结束。
多线程扩容
假设这里有两个线程同 时执行了put()操作,并进入了transfer()环节
从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程 2 rehash 后,就指向了线程2 rehash 后的链表。 然后线程1被唤醒了:
- 执行e.next = newTable[i],于是 key(3)的 next 指向了线程1的新 Hash 表,因 为新 Hash 表为空,所以e.next = null,
- 执行newTable[i] = e,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。
- 执行e = next,将 e 指向 next,所以新的 e 是 key(7)
然后该执行 key(3)的 next 节点 key(7)了:
- 现在的 e 节点是 key(7),首先执行Entry<K,V> next = e.next,那么 next 就是 key(3)了
- 执行e.next = newTable[i],于是key(7) 的 next 就成了 key(3)
- 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(7)
- 执行e = next,将 e 指向 next,所以新的 e 是 key(3)
此时状态为:
然后又该执行 key(7)的 next 节点 key(3)了:
- 现在的 e 节点是 key(3),首先执行Entry<K,V> next = e.next,那么 next 就是 null
- 执行e.next = newTable[i],于是key(3) 的 next 就成了 key(7)
- 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(3)
- 执行e = next,将 e 指向 next,所以新的 e 是 key(7)
这时候的状态如图所示:
很明显,环形链表出现了
Jdk8-扩容
Java8 HashMap扩容跳过了Jdk7扩容的坑,对源码进行了优化,采用高低位拆分转移方 式,避免了链表环的产生。
扩容前:
扩容后:
ConcurrentHashMap
数据结构
与HashMap类似,但是内部在数据写入时,加了同步机制(分段锁)保证线程安全,读操作无锁操作;在扩容时,老数据的转移是并发执行的,扩容效率会更高。
JDK1.7并发安全控制
Java7 ConcurrentHashMap基于ReentrantLock实现分段锁
每个segment都继承了ReentrantLock,相当于每个segment都是一把锁,并发量等于segment的数量。
Java8中 ConcurrentHashMap基于分段锁+CAS保证线程安全,分段锁基于synchronized 关键字实现;
重要成员变量
-
LOAD_FACTOR: 负载因子, 默认75%, 当table使用率达到75%时, 为减少table 的hash碰撞, tabel长度将扩容一倍。负载因子计算: 元素总个数%table.lengh。
-
TREEIFY_THRESHOLD: 默认8, 当链表长度达到8时, 将结构转变为红黑树。
-
UNTREEIFY_THRESHOLD: 默认6, 红黑树转变为链表的阈值
-
MIN_TRANSFER_STRIDE: 默认16, table扩容时, 每个线程最少迁移table的槽位个数
-
MOVED: 值为-1, 当Node.hash为MOVED时, 代表着table正在扩容
-
TREEBIN, 置为-2, 代表此元素后接红黑树。
-
nextTable: table迁移过程临时变量, 在迁移过程中将元素全部迁移到nextTable 上
-
sizeCtl: 用来标志table初始化和扩容的,不同的取值代表着不同的含义:
A: 0: table还没有被初始化
B: -1: table正在初始化
C:小于-1:,表明table正在扩容
D:大于0: 初始化完成后, 代表table最大存放元素 的个数, 默认为0.75*n -
: table容量从n扩到2n时, 是从索引n->1的元素开始迁移, transferIndex代表当前已经迁移的元素下标
-
ForwardingNode:扩容期间, 若table某个元素为null, 那么该元素设置为 ForwardingNode, 当下个线程向这个元素插入数据时, 检查hashcode=MOVED, 就 会帮着扩容
put(key,value)
final V putVal(K key, V value, boolean onlyIfAbsent) {
//Key和value都不可以为空;
if (key == null || value == null) throw new NullPointerException();
//获取在数组中的位置;
int hash = spread(key.hashCode());
int binCount = 0;
//自旋操作,可能会有多个线程同时进来。
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//初始化容器;
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//判断容器中的这个位置是否为空,如果为空,则使用CAS在这个位置插入数据
//可能多个线程同时在往一个位置插入数据,使用CAS
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
//判断容器是否正在扩容,如果在扩容,则帮助扩容
tab = helpTransfer(tab, f);
else {
// f为容器某个位置的首结点。
V oldVal = null;
synchronized (f) {
//加锁,同步;防止同时写入数据
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
//遍历容器插入位置所在的链表;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
//如果key相同,则覆盖。
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
//下一个结点为空,则说明已经到了链表尾部了,在链表尾部插入数据。
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
//判断首节点是否为红黑树结点,是则插入红黑树中。
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
//判断链表长度是否>8,是则转化为红黑树;
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
get(key)
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//获取在容器中的位置
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null)
{
if ((eh = e.hash) == h) {
//首节点哈希值相等,再比较他们的key值。
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
//首节点哈希值相等,key也相等,找到了value值。
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
//首节点的哈希值相等,Key不相等。遍历,链表,找打对应的值
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}