接上一篇HashMap源码解析
HashMap实现原理总结
谈谈你对HashMap的理解?
先从底层数据出发。HashMap实现Map接口,存储键值对。JDK8版本底层结构基于数组+链表/红黑树,当每个桶中元素达到树化阈值8,并且数组总量大于64,由链表转为红黑树;元素小于阈值6,转回链表。针对JDK7版本做了优化,将最坏查询由O(n)降到O(logn)。
其次,HashMap线程不安全。
HashMap的数据插入原理是怎样的?
HashMap怎么设定初始容量大小?
一般如果new HashMap()不传值,默认大小是16,负载因子是0.75。如果自己传入初始大小k,初始化大小为大于k的2的整数次方,例如如果传10,大小为16。
HashMap的哈希函数设计是怎样的?
参考上面hash实现
HashMap底层数组为什么总是2的n次方?
参考上面,主要是减少hash碰撞。
1.8还有别的优化吗?
1、链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表将元素放置到链表的最后;
在jdk1.7中,由于扩容时使用头插法,在并发时可能会形成环状列表,导致死循环,在jdk1.8中改为尾插法,可以避免这种问题,但是依然避免不了节点丢失的问题。
2、扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
将每个桶的元素分别处理成两个高低位链表。
3、在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
扩容的时候为什么1.8不用重新hash就可以直接定位原节点在新数据的位置呢?
为啥扩容两倍?原因就在此,为了不用重新hash。
【分析】假设一个元素经过hash()运算得到值是hash,其在数组的桶是(oldCap-1)&hash位置。
假设oldCap=8,hash=9。
0111 & 1001 = 0001 = 1,元素在1位置。1000 & 1001 = 8 > 0
此时扩容两倍,cap=16
11111 & 1001 = 01001 = 9,元素在9位置。相当于 1 + 8。
上面的例子,如果hash=33,则resize前后元素都在1位置。
HashMap并发时造成死循环问题解析
死循环问题在JDK 1.8 之前是存在的,JDK 1.8 通过增加loHead和loTail进行了修复。在JDK 1.7及之前 HashMap在并发情况下导致循环问题,致使服务器cpu飙升至100%。
HashMap在JDK1.7的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。
for (Entry<K,V> e : table) { //
while(null != e) {
Entry<K,V> next = e.next;
e.next = newTable[i]; // 关键几行代码
newTable[i] = e;
e = next;
}
}
https://cloud.tencent.com/developer/article/1498035?from=15425
HashMap是线程安全的吗?
HashMap线程不安全。例如并发put操作就会存在数据覆盖。
怎么解决这个线程不安全的问题?
Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。
HashTable,是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大;
Collections.synchronizedMap:是使用Collections:集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;
ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。
1.7与1.8的ConcurrentHashMap:实现有什么不同吗?
1.7 ReentrantLock+Segment+HashEntry实现,分段(segment)加锁。两次hash操作,先找段,再找到数据位置。
JDK1.8之后ConcurrentHashMap.取消了Segment:分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。非常接近HashMap。
当竞争同一个分段锁概率小时,反而造成更新等操作长时间等待。
讲讲CAS是怎么保正线程安全的?
CAS(比较与交换,Compare and swap)是一种无锁算法。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS是基于系统命令的原子实现,只会被一个线程更新成功。因为非阻塞,所以没有死锁;没有线程阻塞和上下文切换带来的开销。
CAS会引入ABA问题
假设有一个变量A,修改为B,然后又修改为了A,实际已经修改过了,但CAS可能无法感知,造成了不合理的值修改操作。
AtomicStampedReference,设置版本号。