面试之ConcurrentHashMap

本文详细解析了ConcurrentHashMap的工作原理,包括变量定义、初始化过程、put操作流程、节点hash值范围及扩容机制。通过分析源码,揭示了ConcurrentHashMap如何在多线程环境下高效地进行数据操作和扩容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

https://www.cnblogs.com/yangming1996/p/8031199.html

一、变量

//map
transient volatile Node<K,V>[] table;

//扩容时的临时map,正常情况下null
private transient volatile Node<K,V>[] nextTable;

//基础计数,如果有多线程并发,则需要加上counterCells中的计数
private transient volatile long baseCount;

// -1 : 正在进行初始化
// <-1:  -(1 + 正扩容的线程数)
// 0: 默认值.
// > 0: 扩容阈值
//为什么不单独设置其他的控制变量?
private transient volatile int sizeCtl;

//下一个扩容任务的桶index,从table的尾部开始向前
private transient volatile int transferIndex;

//更新计数器时使用,轻量锁
private transient volatile int cellsBusy;

//并发计数器
private transient volatile CounterCell[] counterCells;

一、初始化

采用的是懒加载的方式,也就是只有在第一次put时才会执行init

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            //正在被其他线程初始化
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                //竞争设置sizeCtl为-1
                try {
                    //假设线程A,执行到2214行,而线程B执行到break,
                    // 则这个判断可以避免其他线程继续初始化
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

二、put操作

1. 如果tab空,则在initTable中,进行单线程的初始化tab,初始化成功后,sc为下次扩容的阈值

2. 如果tab不空,则判断该hash位置是否为空,空则使用cas在该位置设置新节点

3. 如果节点类型是forwardNode,说明该hash位置进行了扩容,则会前去帮助其进行扩容

4. 利用synchronized进行添加节点

5. 添加完节点,进行元素个数+1 & 扩容

6. 正在扩容时,老的hash表所有节点状态都会逐渐变成moved,这样,如果有新的put,则如果是moved状态,其会在moved结束之后,重新加入hash,如果不是moved,则会加入老的hash表,同时加入的时候,会加锁,这样避免了多线程下的数据丢失

节点的hash值范围:

-1: 表示该位置扩容完成

-2: 树节点

其他: 正常的节点

三、在前一段中,我们知道,如果一个线程发现添加的位置发生了扩容,则会加入扩容的线程集合中

如果,不需要协助扩容,则会一直在这里傻傻等待扩容完成,才会返回重新添加新节点

四、扩容

1. 新表的初始化,一定会只有一个线程初始化,(只有收个扩容线程才会transfer(tab, null), 其他调用之前都有加空判断) 

2. 每个线程最少分配16个桶,可以由多个线程操作,但是每个桶只分配给一个线程处理,所以整个操作期间,会更简单

3. 在重新hash时,会使用synchronized关键实现线程安全,当hash到新的位置之后,老的hash表会设置老节点为forwardnode,告诉其他冲突线程,正在rehash,但是不影响hash位置不一致的线程,实现了并发尽可能的大

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            //一个线程最少负责16个桶的rehash
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        //新表的初始化,只有一个线程进行初始化
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            //这个表示新的nextTable
            nextTable = nextTab;
            //从N开始分配桶,所谓的桶,就是tab的一个元素,每次递减stride
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        //true:重新rehash下一个桶
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            //初始化线程领取负责rehash的桶,负责的桶的范围bound...i
            while (advance) {
                int nextIndex, nextBound;
                //完成该任务分配的扩容桶
                if (--i >= bound || finishing)
                    advance = false;
                //没有新的扩容桶
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                //初始化该任务分配的扩容桶的上下界
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }

            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //该线程rehash完自己负责的桶,则退出,此时扩容线程数减-1
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //是否最后一个扩容线程
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            // 0 或 1, 表示新的hash位置在原位置还是新的i+n的位置
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            //寻找到最后一个节点
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

 

### ConcurrentHashMap 面试常见问题与解答 #### 什么是 ConcurrentHashMap? `ConcurrentHashMap` 是 Java 中的一个线程安全的哈希表实现类,适用于多线程环境下的高性能需求场景。它通过细粒度的锁机制以及 CAS(Compare-And-Swap)技术,在保证数据一致性的同时提升了并发性能。 #### JDK 1.8 中 `ConcurrentHashMap` 的主要改进有哪些? 在 JDK 1.8 中,`ConcurrentHashMap` 放弃了传统的基于 Segment 的设计,转而采用了更加轻量级的方式——**Node + CAS + synchronized** 来保障线程安全性[^1]。这种新方法减少了锁的竞争,从而进一步提高了并发效率。 #### `HashMap` 和 `ConcurrentHashMap` 的区别是什么? 两者的主要差异如下: - **线程安全性**: - `HashMap` 不具备任何同步控制措施,因此是非线程安全的。 - `ConcurrentHashMap` 则专门针对高并发场景优化,能够有效防止多个线程同时修改而导致的数据不一致问题。 - **空值支持**: - `HashMap` 允许键和值均为 null。 - 而 `ConcurrentHashMap` 对于 key 和 value 均不允许为 null,否则会抛出 NullPointerException[^2]。 - **内部结构变化**: - 在 JDK 7 及之前版本中,`ConcurrentHashMap` 使用的是分段锁 (Segment Locking) 技术;而在 JDK 8 中改为了基于节点 (`Node`) 的链表或红黑树形式,并引入了 CAS 操作减少锁定范围。 #### 如何理解 `ConcurrentHashMap` 的底层实现原理? 其核心思想可以概括为以下几个方面: 1. 数据分布策略:类似于普通的 Hash 表,计算 hashcode 后定位到具体的 bucket 上; 2. 锁定区域缩小:相比早期版本中的粗暴全表锁定或者按 segment 分割锁定,现在只会在特定条件下才施加独占式的写入权限; 3. 并发读取无阻塞:得益于非阻塞算法的应用,大多数情况下读操作不会受到其他事务的影响可以直接完成; 4. 动态扩容处理:当某个 bin 下面挂载过多元素形成较长链条时,则考虑将其转换成平衡二叉查找树以便加速后续访问速度。 #### 关于迭代器的安全性如何描述? 值得注意的是,尽管 `ConcurrentHashMap` 提供了 fail-safe 特性的 Iterator 实现,这意味着即使集合本身正在经历结构性改变(比如新增删除条目),也不会影响当前正在进行中的遍历过程[^3]。不过需要注意一点就是如果应用程序依赖这些改动后的状态来做决定的话可能就会遇到逻辑错误风险。 ```python from concurrent.futures import ThreadPoolExecutor import threading def test_concurrenthashmap(): from java.util.concurrent import ConcurrentHashMap map = ConcurrentHashMap() def add_item(key, value): map.putIfAbsent(key, value) with ThreadPoolExecutor(max_workers=5) as executor: futures = [] for i in range(10): future = executor.submit(add_item, f"Key-{i}", f"Value-{i}") futures.append(future) # Wait all tasks done. _ = [future.result() for future in futures] print(map.size()) test_concurrenthashmap() ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值