HashMap面试题

一、介绍下 HashMap 的底层数据结构

  • JDK1.8之前的HashMap数据结构:数组 + 链表
  • JDK1.8之后的HashMap数据结构:数组 + 链表 / 红黑树

二、1.8 为什么要改成“数组+链表/红黑树”?

主要是为了提升在hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。

三、那在什么时候用链表?什么时候用红黑树?

  • 插入:默认情况下是使用链表节点。当同一个索引位置的节点在新增后达到9个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点;而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。
  • 移除:当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点

四、为什么链表转红黑树的阈值是8?

阈值为8是在时间和空间上权衡的结果

  • 红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价不值得。
  • 理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,链表中节点个数为8时的概率为 0.00000006,这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字。

五、为什么转回链表节点是用的6而不是复用8?

如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗

六、HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

默认初始容量是16。HashMap 的容量必须是2的N次方,HashMap 会根据我们传入的容量计算一个大于等于该容量的最小的2的N次方,例如传 9,容量为16。

七、HashMap 的容量必须是 2 的 N 次方,这是为什么?

  • 原因1:降低发生碰撞的概率,使散列更均匀。根据 key 的 hash 值计算 bucket 的下标位置时,使用 “与”运算公式:h & (length-1),当哈希表长度为 2 的次幂时,等同于使用表长度对 hash 值取模,散列更均匀;
  • 原因2:表的长度为 2 的次幂,那么(length-1)的二进制最后一位一定是 1,在对 hash 值做“与”运算时,最后一位就可能为 1,也可能为 0,换句话说,取模的结果既有偶数,又有奇数。设想若(length-1)为偶数,那么“与”运算后的值只能是 0,奇数下标的 bucket 就永远散列不到,会浪费一半的空间。

八、负载因子为什么默认值是0.75。

答:在时间和空间上权衡的结果。如果值较高,例如1,此时会减少空间开销,但是 hash 冲突的概率会增大,增加查找成本;而如果值较低,例如 0.5 ,此时 hash 冲突会降低,但是有一半的空间会被浪费,所以折衷考虑 0.75 似乎是一个合理的值。

九、HashMap 的插入流程是怎么样的?

在这里插入图片描述

十、图里刚开始有个计算 key 的 hash 值,是怎么设计的?

拿到 key 的 hashCode,并将 hashCode 的高16位和 hashCode 进行异或(XOR)运算,得到最终的 hash 值。

JDK1.7进行了9次扰动,JDK1.8进行了2次扰动。

JDK1.7代码

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK1.8代码

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

十一、为什么要将 hashCode 的高16位参与运算?

减少哈希碰撞

十二、扩容(resize)流程介绍下?

在这里插入图片描述

十三、对比JDK1.7 JDK 1.8 主要进行了哪些优化?

  1. 底层数据结构从“数组+链表”改成“数组+链表+红黑树”,主要是优化了 hash 冲突较严重时,链表过长的查找性能:O(n) -> O(logn)。
  2. 优化了 hash 值的计算方式,新的只是简单的让高16位参与了运算。
  3. 扩容时插入方式从“头插法”改成“尾插法”,避免了并发下的死循环
  4. 扩容时计算节点在新表的索引位置方式从“h & (length-1)”改成“hash & oldCap”,性能可能提升不大,但设计更巧妙、更优雅。

hash扰动

// JDK 1.7 -- 9次
static int hash(int h) {
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}
// JDK 1.8 -- 2次
static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

十四、扩容重新计算下标

扩容:是根据之前计算过的hashcode重新计算下标,不会重新计算hashcode。扩容后,元素到新数组的位置会出现两种情况

  • 下标位置不变
  • 下标 + 原数组的长度

所以原来一个比较长的链表,经过扩容重新计算下标,就会分布到新数组的两个位置中,那么就会缩短链表的长度。

扩容利用左移一位计算,相当于*2。

1.7

if ((size >= threshold) && (null != table[bucketIndex])) {
  	resize(2 * table.length);
    hash = (null != key) ? hash(key) : 0;
    bucketIndex = indexFor(hash, table.length);
}

1.8

// 如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
if (++size > threshold)   
   resize();

十五、JDK1.7和1.8的改进

1.7采用的是Entry数组

 table = new Entry[capacity];

1.8采用的是Node数组

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

1.7采用的是头插(并发下会导致死循环)

table[bucketIndex] = new Entry<>(hash, key, value, e);

1.8采用的是尾插

 tab[i] = newNode(hash, key, value, null);

十六、ConcurrentHashMap

JDK1.7构造

static final int DEFAULT_INITIAL_CAPACITY = 16;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

static final int DEFAULT_CONCURRENCY_LEVEL = 16;


public ConcurrentHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) // 参数校验
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) { // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值
        ++sshift; //代表ssize左移的次数
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift; // 记录段偏移量
    this.segmentMask = ssize - 1; // 记录段掩码
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)  //Segment 中的类似于 HashMap 的容量至少是2或者2的倍数
        cap <<= 1;
    // create segments and segments[0] // 创建 Segment 数组,设置 segments[0] 只创建了segments[0]
    Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                    (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

JDK1.8构造

//什么也没做
public ConcurrentHashMap() {
}

JDK1.7 put
使用Segment和CAS保证线程安全

//value 不能为null
if (value == null)
    throw new NullPointerException();

//非第一次添加  HashEntry数组取的是Segment[0]的长度
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];


private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    //第一次判断
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        //第二次判断
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                 //使用CAS(比较并交换)保证线程安全
                 //参数:Segment  要操作的值 期望值 赋值
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

//真正的put
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
	HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash; // 计算要put的数据位置
        HashEntry<K,V> first = entryAt(tab, index);  // CAS 获取 index 坐标的值
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {  // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null) // first 有值没说明 index 位置已经有值了,有冲突,链表头插法。
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)  // 容量大于扩容阀值,小于最大容量,进行扩容
                    rehash(node);
                else
                    setEntryAt(tab, index, node);  // index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

JDK1.8 put

public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
//使用synchroized和cas锁保证线程安全
final V putVal(K key, V value, boolean onlyIfAbsent) {
	//key和value都不允许为null
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;  // f = 目标位置元素
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // 数组桶为空,初始化数组桶(自旋+CAS)
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))  // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {  // 使用 synchronized 加锁加入节点
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { // 说明是链表
                        binCount = 1;
                        for (Node<K,V> 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<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)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;
}

CurrentHashMap1.7和1.8

  • JDK1.7中ConcurrentHashMap采用了数组+分段锁的方式实现。
  • JDK1.8中ConcurrentHashMap 相对于 Java7 来说变化比较大,不再是之前的 Segment 数组+ HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。

总结

  • Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是Segment 的个数一但初始化就不能改变。
  • Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值