JDK1.7—》哈希表,链表
JDK1.8—》哈希表,链表,红黑树— JDK1.8之后,当链表长度超过8使用红黑树。
非线程安全
0.75的负载因子,扩容必须为原来的两倍。
默认大小为16,传入的初始大小必须为2的幂次方的值,如果不为也会变为2的幂次方的值。
根据HashCode存储数据。
负载因子0.75 如果容量大大0.75则扩容为原来的两倍。
扩容因此 0.75
空间利用率和时间效率在0.75的时候达到了平衡。
在统计学上0.693是最佳的选择。然后可能更想着有空间利用率,而且在。Net语言中 hashmap的负载因子是0.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 e : table) {
//循环原Entry线性链表
while(null != e) {
Entry 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 e : table) { //第一次for循环数组下标为0 ; e == null 跳出while循环 ;第二次for循环数组下标为1;e = 3;
while(null != e) { //第一次while循环 e = 3;e != null
Entry 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 e : table) { //第一次for循环数组下标为0 ; e == null 跳出while循环 ;第二次for循环数组下标为1;e = 3;
while(null != e) { // 第二次while 循环 e = 7; e != null;
Entry 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—>-两个线程同时并发的对原数组扩容。由于链表都使用头插法,两个线程在用指针指向后,会形成循环链表。 然后再新数据进入的时候,会先从链表上找是否存在对应的key。然后在循环链表中一直死循环,如法插入。
JDK1.7—>-两个线程同时并发的对原数组扩容。由于链表都使用头插法,两个线程在用指针指向后,会形成循环链表。 然后再新数据进入的时候,会先从链表上找是否存在对应的key。然后在循环链表中一直死循环,如法插入。
1:假设线程A在某处挂起
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {
while(null != e) {
Entry 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的数据结构,就会在这里发生死循环
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 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();
}
}
}
其次,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时造成死循环。