数据结构之Map

198 阅读4分钟

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、新建一个新的链表去存储要迁移的数据,并且没有将原链表直接引用到新链表