1.hashmap为什么要rehash?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这段代码叫“扰动函数”。右移16位,将高16位与低16位做异或,以此来加大低位的随机性,从而减少碰撞次数。
因为数组的索引是 (n - 1) & hash ,n是数组长度,也就是实际上数组的索引取得是hash的低位,低位的随机性决定了碰撞次数。
2.为什么数组长度必须是2的幂
因为这样刚好可以作为hash的低位掩码,比如cap=16,则掩码就是1111
3.为什么hashmap是线程不安全的?
jdk1.7 并发resize时会出现链表循环引用,导致get时出现死循环,jdk1.8已经修复了。 juejin.cn/post/684490…
4.jdk1.8中concurrentHashMap为什么放弃分段锁?
段Segment继承了重入锁ReentrantLock,有了锁的功能,每个锁控制的是一段,当每个Segment越来越大时,锁的粒度就变得有些大了。
分段锁的优势在于保证在操作不同段 map 的时候可以并发执行,操作同段 map 的时候,进行锁的竞争和等待。这相对于直接对整个map同步synchronized是有优势的。
缺点在于分成很多段时会比较浪费内存空间(不连续,碎片化);
操作map时竞争同一个分段锁的概率非常小时,分段锁反而会造成更新等操作的长时间等待; 当某个段很大时,分段锁的性能会下降。
5.concurrentHashMap怎么保证线程安全?
CAS+synchronized,
当没有发生hash冲突时,用CAS+自旋更新value
当发生hash冲突时,用synchronized增加链节点或者红黑树节点
6.concurrentHashMap为什么选择synchronized而没用ReentrantLock?
(1)节省内存空间
如果使用ReentrantLock则需要节点继承AQS来获得同步支持,增加内存开销,而1.8中只有头节点需要进行同步。
(2)方便后期优化
synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。
7.为什么重写equals方法就必须要重写hashcode方法?
如果不重写hashcode,那就有可能导致equals返回true,而hashcode值却是不一样的。这样两个相等的对象(equals方法相同)放在hashmap里面确对应两个不同的value。
8.jdk1.8和jdk1.7在扩容时有什么区别?
我们先看一下jdk1.7的源码
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
//保留要转移指针的下一个节点
Entry<K,V> next = e.next;
//计算出要转移节点在hash桶中的位置
int i = indexFor(e.hash, newCapacity);
//使用头插法将需要转移的节点插入到hash桶中原有的单链表中
e.next = newTable[i];
//将hash桶的指针指向单链表的头节点
newTable[i] = e;
//转移下一个需要转移的节点
e = next;
} while (e != null);
}
分析源码可以知道两点:
1、链表采用的头插法
2、直接将原来的next节点应用到新表的节点
上述两点多线程时会造成死循环,主要是在设置next节点时
下面看1.8代码
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//新建一个链表,用于迁移过程中的存储
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//判断高低位,然后尾插,是插入到新的链表,并没有将当前节点的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;
}
}
}
}
分析源码可以知道两点:
1、链表采用的尾插法
2、新建一个新的链表去存储要迁移的数据,并且没有将原链表直接引用到新链表