导读
前面文章一、深入理解-Java集合初篇 中我们对Java的集合体系进行一个简单的分析介绍,上两篇文章二、Jdk1.7和1.8中HashMap数据结构及源码分析 、三、JDK1.7和1.8HashMap数据结构及源码分析-续 中我们分别对JDK1.7和JDK1.8中HashMap的数据结构、主要声明变量、构造函数、HashMap的put操作方法做了深入的讲解和源码分析。
四、深入理解Java中的HashMap「网易面试快答」文章中主要针对面试中常见的面试问题进行简单解答。
五、深入理解JDK1.7中HashMap哈希冲突解决方案 和 六、深入理解JDK1.8中HashMap哈希冲突解决方案 中对HashMap中哈希冲突及减少哈希冲突的解决方案做详细的介绍,并通过源码加深大家的理解。
本篇文章我们将要介绍JDK1.7中HashMap的扩容机制及扩容过程中可能出现的死锁及数据丢失问题。
如果大家在面试中针对Java集合或者Java中的HashMap大家还有什么疑问或者其他问题,可以评论区告诉我。
简单介绍
JDK1.7—》哈希表,链表
JDK1.8—》哈希表,链表,红黑树— JDK1.8之后,当链表长度超过8使用红黑树。
非线程安全
0.75的负载因子,扩容必须为原来的两倍。
默认大小为16,传入的初始大小必须为2的幂次方的值,如果不为也会变为2的幂次方的值。
根据HashCode存储数据。
HashMap扩容机制-为什么负载因子默认为0.75f?
负载因子0.75 如果容量大大0.75则扩容为原来的两倍。
扩容因此 0.75
空间利用率和时间效率在0.75的时候达到了平衡。
在统计学上0.693是最佳的选择。然后可能更想着有空间利用率,而且在。Net语言中 hashmap的负载因子是0.7.
JDK1.7-根据条件判断是否对哈希表进行扩容
如果当前哈希表大小超过了阀值,并且新entry要插入的哈希桶的位置不为空
则把哈希表大小扩展为原来的两倍。
当哈希表中数据容量达到阀值,则使用一个新数组来获得一个更大的容量。
如果当前容量是允许的最大容量(2的30次幂),则该方法不会再继续扩大容量,
但是他会把负载门槛设置为Integer.MAX_VALUE.这只为了防止后续再进行扩容操作。
新传入的容量的值必须为2的n次幂,并且必须大于当前数组的容量。
如果当前数组容量已经允许的最大容量(2的30次幂),则新传入的值被忽略。
/**
当哈希表中数据容量达到阀值,则使用一个新数组来获得一个更大的容量。
如果当前容量是允许的最大容量(2的30次幂),则该方法不会再继续扩大容量,
但是他会把负载门槛设置为Integer.MAX_VALUE.这只为了防止后续再进行扩容操作。
* 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.
*新传入的容量的值必须为2的n次幂,并且必须大于当前数组的容量。
如果当前数组容量已经允许的最大容量(2的30次幂),则新传入的值被忽略。
* @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) {
//把原哈希表数组赋值给oldTable
Entry[] oldTable = table;
//把原哈希表容量赋值给oldCapacity
int oldCapacity = oldTable.length;
//如果当前的哈希表容量已经达到允许的容量最大值(2的30次幂),则不再进行扩容
//且把当前哈希表的负载门槛设置为Integer的最大值。返回,跳过。
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建一个新的哈希数组,容量为新传入的容量值
//该容量值必须是2的n次幂,且大于原数组容量大小
Entry[] newTable = new Entry[newCapacity];
//开始把原哈希表数组数据转入新创建的哈希表数组中
//在开始转存前要先根据新的数组容量及相应的算法得出是否使用哈希干扰掩码
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//转存完成后把新表内容放到HashMap的哈希表值中
table = newTable;
//设置当前容量下的负载门槛
//(新容量 * 负载因子)的值与(HashMap允许的最大容量(2的30次幂)+1) 进行比较,
//取值小的那一个
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
哈希表数据转存:
把哈希表中的所有entry从当前的哈希表转入到新的哈希表中
HashMap扩容时避免rehash的优化
扩容迁移的时候要进行rehash –影响效率。
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
//获取新哈希表的容量
int newCapacity = newTable.length;
//循环原哈希表
for (Entry<K,V> e : table) {
//循环原Entry线性链表
while(null != e) {
Entry<K,V> next = e.next;
//根据是否启用rehash判断是否为每一个key生成新的哈希值
//如果当前entry的key等于null,则重新设置当前entry的哈希值为0
//如果不为null,则对当前entyr的哈希值根据哈希干扰因子(HashSeed)进行重
//新计算赋值
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//根据新的哈希值和新的容量计算该entry应该存放的数组下标位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
我们对以上的语句使用一个示例进行解读:
1.分析图解:
1:为了方便计算,假设hash算法为key mod链表长度;
2:初始时数组长度2,key = 3, 7, 5 ,初始在表table[1]节点;3:然后resize后,hash数组长度为4
2.第一次while循环
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length; //newCapacity = 4;
for (Entry<K,V> e : table) { //第一次for循环数组下标为0 ; e == null 跳出while循环 ;第二次for循环数组下标为1;e = 3;
while(null != e) { //第一次while循环 e = 3;e != null
Entry<K,V> next = e.next;// next = e.next = 7;
if (rehash) { //暂时忽略rehash
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity); //新数组下标 i = (3 % 4) = 3;
e.next = newTable[i]; //e.next = newTable[i] = newTable[3] = null;
newTable[i] = e; // newTable[3] = 3;
e = next; //e = next = 7;
}
}
}
3.第二次while循环
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length; //newCapacity = 4;
for (Entry<K,V> e : table) { //第一次for循环数组下标为0 ; e == null 跳出while循环 ;第二次for循环数组下标为1;e = 3;
while(null != e) { // 第二次while 循环 e = 7; e != null;
Entry<K,V> next = e.next;// next = e.next = 5;
if (rehash) { //暂时忽略rehash
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity); //新数组下标 i = (7 % 4) = 3;
//e.next = newTable[i] = newTable[3] = 3;
//这里要清楚newTable[3]的值已经是新数组中的3.
//因此这一步的操作实际上是把当前的e(7)的next指针指向了3. -也就是常说的头部插入法
e.next = newTable[i];
newTable[i] = e; //newTable[3] = 7;
e = next; //e = next = 5; 后面while循环及for循环以此类推
}
}
}
HashMap的转存使用头部插入法。
分析图解:
1:为了方便计算,假设hash算法为key mod链表长度;
2:初始时数组长度2,key = 3, 7, 5 初始在表table[1]节点;
3:然后resize后,hash数组长度为4
如果不发生异常,正常结果为:
JDK1.7-Hashmap扩容死锁问题
JDK1.7—>-两个线程同时并发的对原数组扩容。由于链表都使用头插法,两个线程在用指针指向后,会形成循环链表。 然后再新数据进入的时候,会先从链表上找是否存在对应的key。然后在循环链表中一直死循环,如法插入。
1:假设线程A在某处挂起
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];
//线程A在此处挂起
newTable[i] = e;
e = next;
}
}
}
当A挂起后,线程B正常执行完
由于线程B已经执行完毕,根据Java内存模型,现在newTable和table中的Entry都是主存中最新值:7.next=3,3.next=null。
此时切换回线程A上,在线程A挂起时继续执行
newTable[i]=e ----> newTable[3]=3
e=next ----> e=7
继续下一次循环,e=7
next=e.next ----> next=3【从主存中取值】
e.next=newTable[3] ----> e.next=3【从主存中取值】
newTable[3]=e ----> newTable[3]=7
e=next ----> e=3
e不为空继续下一次循环 e=3
next=e.next ----> next=null
e.next=newTable[3] ----> e.next=7 即:3.next=7
newTable[3]=e ----> newTable[3]=3
e=next ----> e=null
此次循环后3.next = 7 但上一步 7.next =3 行成环行链表
在后续操作中只要涉及轮询hashmap的数据结构,就会在这里发生死循环
JDK1.7-HashMap扩容死锁问题复现
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* JDK1.7 HashMap死锁问题复现
*/
public class TestHashMapThread implements Runnable{
//为了尽快扩容,设置初始大小为2
private static Map<Integer,Integer> map = new HashMap<>(2);
private static AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
while (atomicInteger.get() < 1000000){
map.put(atomicInteger.get(),atomicInteger.get());//往线程中插入值
atomicInteger.incrementAndGet();//递增
}
}
public static void main(String[] args){
for(int i = 0; i < 10 ; i++){
//启动十个线程去做处理
new Thread(new TestHashMapThread()).start();
}
}
}
JDK1.7-HashMap扩容数据丢失问题
其次,1.7中扩容还会出现数据丢失
模拟另外一种情况
同样线程A在固定位置挂起
线程B完成扩容
同样注意由于线程B执行完成,newTable和table都为最新值:5.next=null。
此时切换到线程A,在线程A挂起时:e=7,next=5,newTable[3]=null。
执行newtable[i]=e,就将7放在了table[3]的位置,此时next=5。接着进行下一次循环:
e=5
next=e.next ----> next=null,从主存中取值
e.next=newTable[1] ----> e.next=5,从主存中取值
newTable[1]=e ----> newTable[1]=5
e=next ----> e=null
将5放置在table[1]位置,此时e=null循环结束,3元素丢失,并形成环形链表。并在后续操作hashmap时造成死循环。
往期文章链接
Java集合
三、JDK1.7和1.8HashMap数据结构及源码分析-续
六、深入理解JDK1.8中HashMap哈希冲突解决方案
Java-IO体系
一、C10K问题经典问答
二、java.nio.ByteBuffer用法小结
三、Channel 通道
四、Selector选择器
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
七、IDEA的maven项目的netty包的导入(其他jar同)
八、JAVA IO/NIO
九、网络IO原理-创建ServerSocket的过程
十、网络IO原理-彻底弄懂IO
十一、JAVA中ServerSocket调用Linux系统内核
十二、IO进化过程之BIO
十三、Java-IO进化过程之NIO
十四、使用Selector(多路复用器)实现Netty中Reactor单线程模型
十五、使用Selector(多路复用器)实现Netty中Reactor主从模型
十六、Netty入门服务端代码
十七、IO进化过程之EVENT(EPOLL-事件驱动异步模型)
如需了解更多更详细内容也可关注本人CSDN博客:不吃_花椒
Java集合还需要学习的内容