HashMap相关面试问题

HashMap原理

hashMap内部包含了一个Entry类型的数组table

transient Entry[] table;

table数组中每个索引位置(可以将每个索引位置看成是一个桶bucket)存储着一条链表或者一棵红黑树。hashMap通过哈希算法计算出key对应的索引位置,不同的key计算出来的索引位置有可能出现冲突,拉链法和线性探测法可以解决位置冲突问题,hashMap采用的是拉链法。

拉链法的大致思路:使用哈希算法计算出索引位置,如果该索引位置对应的链表为空,则直接把键值对作为链表头节点,如果不为空(说明发生了碰撞),则遍历链表看是否有相同的key,如果有则替换掉该key对应的value值。如果链表中没有相同的key,则将该Entry节点插入到原链表的头部(头插法)。

hashCode的计算

key.hashCode()是key自带的hashCode函数,返回一个32位二进制值。

  1. 调用key.hashCode()
  2. hashCode值高16位异或低16位(右移16位然后与原先的hashCode值异或,即自己的高半区与低半区做异或,这样混合后的低位掺杂了高位的部分信息):return (key==null)?0:h=key.hashCode()^h>>>16;
  3. 取模运算:h&(length-1) 其中length为table数组长度。h&(length-1) 等价于h%length

HashMap相关面试问题_第1张图片

Java1.8中HashMap中扩容

首先在HashMap中规定数组的长度一定为2次幂,扩展后的数组长度为原先的2倍(保证新数组长度也是2次幂)。

假设原数组的长度old capacity为16,扩容后new capacity为32=2*16:

old capacity :00010000

new capacity :00100000

对于一个key:

  • 如果它的哈希值在第5位为0,则取模后的索引位置与原先一致
  • 如果为1,那么取模后的位置为原来的“ 索引位置+old capacity"

这样我们就无需重新计算hash了,只需要查看某个bit位置为1还是0就可以知道其新的索引位置。

HashMap中链表和红黑树之间的转换

当同一个数组位置下的链表长度大于8时,为了提高查询和修改效率,会将链表转换为红黑树。而当红黑树节点数量小于6时,会将红黑树转换为链表结构。

HashMap中插入空值

HashMap中插入key=null的Entry节点时会调用putForNullKey方法直接去遍历table[0]位置的链表,如果已经存在key为null的Entry节点,则将其value替换掉,否则调用addEntry方法头插一个节点到table[0]位置。

ps:HashTable不能插入key=null的键值对。

HashMap为什么线程不安全

  • 两个线程对同一个table数组索引位置添加节点,其中一个线程的写入操作会被另外一个线程的写入操作覆盖,造成写入丢失。
  • 两个线程对同一个table数组索引位置删除节点,其中一个线程的删除操作会被另外一个线程的删除操作覆盖,造成删除丢失。
  • 两个线程同时开始resize,如果线程A先完成resize的情况下,线程B在resize的过程中很有可能会引用到线程A执行resize后的table,造成错误。

ConCurrentHashMap如何实现线程安全

java1.7使用Segment分段锁机制实现并发安全,每个Segment维护多个bucket,即多个索引位置,一个segment锁只能同时被一个线程拥有。默认采用16个分段锁,即并发度为16。

java1.8采用CAS+synchronized来保证并发安全。

举例put()方法:

首选根据key计算出对应的hashcode,然后得到其table对应的索引位置。

假设f为当前key定位到的索引位置的头节点node,如果头节点为null(即链表为空),则利用CAS操作尝试写入,失败则进行自旋直到成功。

如果头节点不为null(说明链表不为空),此时需要使用synchronized关键字将链表的头节点锁定,防止其他线程同时对该链表进行修改。然后再遍历链表进行对应的写入操作。

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node[] tab = table;;) {
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)//table数组没有初始化,就进行初始化
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//头节点为null则尝试使用cas写入
                if (casTabAt(tab, i, null,
                             new Node(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)//是否需要进行resize
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {//头节点f不为null,锁住头节点(即锁住该链表)
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node p;
                            binCount = 2;
                            if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)//是否需要进行红黑树转换
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

HashMap与HashTable的区别

  • hashMap不是线程安全的,hashTable是线程安全的,
  • 通过使用synchronized关键字来修饰所有的修改方法来实现线程安全。
  • hashMap允许null值,而hashTable不允许null值(key和value都不可以)
  • hashMap基于AbstractMap类,而hashTable基于Dictionary类

ConCurrentHashMap与HashTable区别

  • 两者都是线程安全的,但是hashTable锁住的是整个map,效率低下。而ConcurrentHashMap使用的是cas+synchronized机制,不会锁定整个map,而是锁定table数组位置对应的链表。
  • 一般不要使用hashTable,推荐使用ConCurrentHashMap。

你可能感兴趣的:(java,红黑树,面试,并发)