ConcurrentHashMap 实现原理是什么?面试题学会这些赢麻了

72 阅读4分钟

在这个金九银十的时期,应该有不少小伙伴都在为面试准备着

所以我也整了一些面试文档

短期面试攻略 这样背面试题事半功倍简直赢麻了

图片.png

图片.png

需要的小伙伴三联哦!

我们现在继续讲讲ConcurrentHashMap 实现原理是什么?

JDK1.7

在JDK1.7中,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。在 JDK1.7 中,ConcurrentHashMap 采用 Segment + HashEntry 的方式进行实现,

结构如下:一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁

01.jpg

  1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;

  2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

JDK1.8

JDK1.8 中,放弃了 Segment 臃肿的设计,取而代之的是采用 Node + CAS +Synchronized 来保证并发安全进行实现,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。

结构如下:

02.jpg 附加源码,有需要的可以看看

插入元素过程源码解读:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 检查参数,如果key或value为null则抛出NullPointerException
    if (key == null || value == null)
        throw new NullPointerException();
    
    // 对key的hashCode进行扩展处理,以增加散列性能
    int hash = spread(key.hashCode());
    int binCount = 0; // 记录链表长度
​
    for (Node<K,V>[] tab = table;;) { // 无限循环,直到成功插入或者扩容
        Node<K,V> f; int n, i, fh;
​
        // 如果table为空,进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        
        // 如果桶位置为空,直接将新节点插入到桶中
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break; // 插入成功后跳出循环
        }
        
        // 如果桶处于扩容状态,则帮助进行扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        
        else {
            V oldVal = null;
            // 使用synchronized关键字对桶上的链表进行同步操作
            synchronized (f) { 
                if (tabAt(tab, i) == f) { // 再次检查桶中的第一个节点是否是f
                    if (fh >= 0) {
                        // 遍历链表,查找是否有相同的key存在
                        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; // 找到相同的key,记录旧值
                                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; // 返回null表示没有旧值
}

putVal方法用于将键值对插入ConcurrentHashMap中。在并发环境下,通过对不同的桶进行加锁操作,保证了线程安全性。具体的插入过程如下:

  1. 检查参数,如果key或value为null则抛出NullPointerException。
  2. 对key的hashCode进行扩展处理,以增加散列性能。
  3. 进入无限循环,直到成功插入或者扩容。
  4. 如果table为空,进行初始化。
  5. 如果桶位置为空,直接将新节点插入到桶中。
  6. 如果桶处于扩容状态,则帮助进行扩容。
  7. 如果桶中是链表节点(不是树形节点),遍历链表查找是否有相同的key存在。
  8. 如果桶中是树形节点,插入或更新树形节点。
  9. 插入或更新成功后,根据链表长度判断是否需要将链表转换为树形节点。
  10. 更新计数器。
  11. 返回旧值。

在高并发场景下,ConcurrentHashMap通过细粒度的锁和分段的结构,提供了更好的并发性能和扩容能力,同时保持线程安全。