HashMap的基本数据结构是什么?
HashMap是基于数组+链表+红黑树(JDK1.8)实现的。
主干为一个存储Key-Value键值对的数组,存储位置由key经过Hash算法生成,冲突时以头插法的形式生成链表,当链表长度大于8时链表结构转换为红黑树。
HashMap有哪几个关键的变量?分别起什么作用?
关键变量:1)HashMap的当前长度Capacity,默认值为16。2)容量因子Loadfactor,默认值为0.75。3)最大有效容量Threshold。Threshold=Capacity×Loadfactor。每次做完新增键值对操作会拿当前键值数与Threshold进行比较,如果当前键值数>=Threshold,对HashMap执行扩容操作。
HashMap链表结构有什么特点?
即使HashMap定位哈希桶索引位置的算法再合理也无法避免冲突,所以同一个哈希桶会出现链表过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能,所以在Java8中,对数据结构做了进一步的优化,引入了红黑树。当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能。
HashMap如何实现扩容?
在一个PUT操作完成之前,会拿HashMap的当前数据量与最大容量进行比较,如果HashMap.Size>=threshold,HashMap会执行扩容操作。扩容主要通过Resize()方法来实现:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //定义旧表并把原本的赋值给他
int oldCap = (oldTab == null) ? 0 : oldTab.length;//如果旧表容量为null就初始0
int oldThr = threshold;//旧表的阀值
int newCap, newThr = 0;//定义新表的容量和新表的阀值
//进入条件:正常扩容
if (oldCap > 0) {//如果旧表容量大于0,这个情况就是要扩容了
//进入条件:已达到最大,无法扩容
if (oldCap >= MAXIMUM_CAPACITY) {//如果容量已经大于等于1<<30
threshold = Integer.MAX_VALUE;//设置阀值最大
return oldTab;//直接返回原本的对象(无法扩大了)
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >=DEFAULT_INITIAL_CAPACITY)
//旧表容量左移一位<<1,且移动之后处于合法的范围之中。新表容量扩充完成
newThr = oldThr << 1; // 新表的阀值也扩大一倍。
//进入条件:初始化的时候使用了自定义加载因子的构造函数
} else if (oldThr > 0)
// 这里如果执行的情况是原表容量为0的时候,但是阀值又不为0。
//hashmap的构造函数不同(需要设置自己的加载因子)的时候会触发。
newCap = oldThr;
//进入条件:调用无参或者一个参数的构造函数进入默认初始化
else {
// 如果HashMap默认构造就会进入下面这个初始化,第一次put也会进入下面这一块。
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//初始化完成。
}
//进入条件:初始化的时候使用了自定义加载因子的构造函数
if (newThr == 0) {
float ft = (float)newCap * loadFactor;//新表容量*加载因子
newThr = (newCap < MAXIMUM_CAPACITY &&
ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//确定新的阀值
//开始构造新表
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//进入条件:原表存在
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {//开始遍历
oldTab[j] = null;//旧表原本置空
if (e.next == null)//不存在下个节点,也就是目前put的就是链表头
newTab[e.hash & (newCap - 1)] = e;//把该对象赋值给新表的某一个桶中
//进入条件:判断桶中是否已红黑树存储的。如果是红黑树存储需要宁做判断
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//进入条件:如果桶中的值是合法的,也就是不止存在一个,也没有触发红黑树存储
else {
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;//获取下一个对象信息
//因为桶已经扩容了两倍,所以以下部分是按一定逻辑的把一个链表拆分为两个链表,放入对应桶中。
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
这个方法实现了两个操作:1,新建一个Entry空数组,长度是原数组的2倍。2,遍历原数组,将所有的Entry重新执行Hash运算后存到新的数组。
PUT方法如何实现?
Java8与Java7相比HashMap有什么改进?
1.JDK1.8引入了红黑树,当链表长度大于8时,链表结构改转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能。
2.JDK1.8优化了高位运算,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),提高数据分布的离散性。
3.JDK1.8在扩容HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,而是通过新增的一个位的值来确定bucket的位置,省去了重新计算hash值的时间,由于新增的一位的值是0还是1是随机的,所以还能确保数据离散性。
HashMap为什么不是线程安全型的?
HashMap在扩容的时候需要执行两个步骤:1,新建一个Entry空数组,长度是原数组的2倍。2,遍历原数组,将所有的Entry重新执行Hash运算后存到新的数组。在高并发的情况下在将原数组数据复制到新数组的过程中容易出现环形链表导致死循环。
在高并发情况下如何实现线程安全的Map?
由于HashMap在高并发时在做插入操作可能会出现环形链表,所以在多线程的系统中不能使用HashMap,要避免这个问题有几个办法,比如改用HashTable或者使用Collections.sychronizeMap,但是这两种方式无论是读操作还是写操作都会给整个集合加锁,导致同一时间的其他操作被阻塞,这很影响性能,所以在高并发情况下,为了兼顾线程安全和性能,通常会使用ConcurrentHashMap。
ConcurrentHashMap怎么保证线程安全?
从数据结构来说ConcurrentHashMap是一个二级哈希表,在一个总的哈希表下面有若干个子哈希表。
在ConcurrentHashMap中,每一个Segment都是一个独立的个体,在高并发情况下,不同Segment之间的并发操作互不影响,同一Segment的读写锁可以并发执行的,只有对同一个Segment并发写入的时候才需要上锁,而且锁只是针对Segment,这样在保证线程安全性的同时降低了锁的粒度,让并发操作更有效率
ConcurrentHashMap怎么实现高性能读写?
get方法:
1.为输入的key做hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象。
3.再次通过Hash值定位到Segment中数组具体位置(这一步相当于HashMap的get操作)。
put方法:
1.为输入key做hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象。
3.获得可重入锁。
4.再次通过hash值,定位到Segment中数组的具体位置。
5.插入或覆盖HashEntry对象。(4.5相当于HashMap的put操作)
6.释放锁。
ConcurrentHashMap如何确保一致性?
由于每一个Segment在put操作的时候会单独加锁,这样的结构通常会在计算总量的时候出现一致性问题。ConcurrentHashMap的size方法是一个嵌套循环,逻辑如下:
1.遍历所有的Segment。
2.把Segment的数据累加起来。
3.把Segment的修改次数累加起来。
4.判断所有的Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新计算,尝试次数+1;如果不是,说明没有修改,统计结束。
5.如果尝试次数超过阀值,则对每一个Segment加锁,再重新统计。
6.再次判断所有Segment的总修改次数是否大于上一次的修改总数,由于已经加锁,次数一定和上一次相等。
7.释放锁,统计结束。
这也是乐观锁悲观锁的思想,先乐观的假设没有并发的修改,当尝试到一定的次数之后,才悲观的认为有修改,锁住所有的Segment保证强一致性。