面试宝典——HashMap和ConcurrentHashMap

397 阅读4分钟

HashMap

HashMap是由数组+链表(红黑树)组成,主要关注一下几个参数:

// 默认的Map容量,是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 这个是负载因子,负载因子是经过时间和空间平衡后得出的一个数,当map中的key的个数,达到 capacity * factor 时,就以为需要扩容,这个因子虽然可以修改,但是非特别和特殊场景,不要改动这个数,改动之后,极大可能会影响性能。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表进化成红黑树时,链表的长度必须要大于等于8个
static final int TREEIFY_THRESHOLD = 8;
// 红黑树退化成链表时,树节点的个数必须要小于等于6个才可以退化
static final int UNTREEIFY_THRESHOLD = 6;
// 这个值是面试常问的点:链表的节点个数仅仅超过8个,是不会立马进化成为红黑树的,还要满足当前map中节点的总数要大于等于64,才能将超过8个的链表进化成红黑树,小于64时,扩容更简单,效率更高
static final int MIN_TREEIFY_CAPACITY = 64;


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // HashMap就是数组加链表的结构,只不过数组里面放的是一个Node节点
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //步骤1: 数组为空或者长度为0,则直接扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //步骤2: 计算Hash值,p表示出现的这key在table中的位置,如果这个位置为空,则直接创建一个新的节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 下面都是找节点在数组或者链表中的位置
            Node<K,V> e; K k;
            //步骤3:tab[i]的位置的hash值跟我们计算的hash值一样,这边采用了两种方式判断key是否一致,== 和equal 两种,如果key一样,则直接替换里面的值就行了,直接到步骤6
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            //步骤4: 如果已经是一棵红黑树,则需要往树里面添加新的节点
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //步骤5: 不是红黑树,还是链表,则需要进行判断
                for (int binCount = 0; ; ++binCount) {
                    // e 是 p的下一个节点,如果为空,表示找到链表最后了,新建一个新的节点,同时判断是不是达到红黑树的调整阈值,如果达到了,则需要调整成为一棵红黑树,也就是treeifBin方法实现
                    if ((e = p.next) == null) {
                    // 这个地方跟1.7版本不一样,这个地方是尾不插入,老版本这个地方是头部插入,
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 这个地方要点击去看一下,这个地方有一个最小树型化阈值,需要判断,默认是64,4* TREEIFY_THRESHOLD,在当前tab的容量小于这个值时,只进行扩容,不进行树型化,只有超过这个数之后,链表才会向着树进行转化。
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果在链表中找到了这个节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //步骤6: 如果e 不是null,说明找到节点了,则直接替换数据即可,替换数据并不会引起modCount的改变,也就是说modCount只会在有结构变化的时候,才会更改
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 步骤7:如果key的数量超过了设定的阈值,如要进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

image.png

扩容问题:

在java 1.7版本中,采用的头插节点的方式进行扩容,头插链表的方式,在多线程环境下,扩容容易出现循环,且头插节点会改变节点的顺序。

在java 1.8版本中,移除了头插的方式,改为尾插,尾插节点,不会改变节点的顺序,不会出现死循环问题,但是HashMap本身不是多线程安全,在插入时,步骤2这一步,如果在多线程环境下,AB两个线程同时对同一个Key进行数据覆盖,会导致一个线程的数据丢失。

面试点:

1、HashMap线程不安全的原因

答:扩容时容易造成数据丢失和死循环,即便是在1.8版本中,修改了尾插的方式,在多线程环境下,也容易造成数据丢失,举例就是在两个线程同时断定key存在某一位置的时候,都会进行覆盖数据,造成了数据的丢失。

2、HashMap中容量为2的幂次的原因

答:两点:1、hashCode的计算更加方便,更加均匀分布 2、扩容时,以2倍的方式进行扩容,只需将部分数据,移动到新的位置上就可以了,不需要全部移动,减少了移动的次数。

3、Java1.7和1.8的改动

头插改为尾插,引入红黑树

4、链表转化红黑树的条件

链表长度大于等于8个,其次是总的节点的数量要达到64个,才允许转换。

5、HashMap和HashTable的区别

首先:HashMap是线程不安全的,Hashtable是线程安全的 其次:HashMap里面是允许存在一个key为Null的,Hashtable里面是不允许的,而且Hashtable也不允许value值为null,HashMap并没有这个限制, 第三:Hashtable使用了安全失败的机制(fail-soft),而HashMap使用了快速失败机制(fail-fast),HashMap会维护一个modCount和expectedmodCount,两个值,在迭代器迭代过程中,一旦HashMap有修改,则会抛出异常。 第四:初始的容量不同,HashMap是16,Hashtable是11

ConcurrentHashMap

ConcurrentHashMap是一个线程安全的并发容器,底层的存储机制,跟HashMap基本一致,也是采用了数组+链表或者红黑树的结构进行存储,线程安全的本质,CAS+synchronized

Put的时候,会使用CAS的方式将数据放到table中,如果数据不存在,则会使用synchronized的方式,将数据插入到链表中