ConcurrentHashMap高频面试题及源码分析

120 阅读9分钟

高频面试题

  • ConcurrentHashMap的实现原理是怎样的?是如何保证线程安全的?
  • ConcurrentHashMap在哪些地方做了并发控制?
  • 为什么ConcurrentHashMap不允许null值?
  • ConcurrentHashMap是如何保证fail-safe的?
  • ConcurrentHashMap为什么在JDK1.8废弃分段锁?
  • ConcurrentHashMap为什么在JDK1.8使用synchronized而不是ReentrantLock?

今天我们带着上述面试高频被问到的问题,结合源码来一探究竟吧

put方法

public V put(K key, V value) {  
    return putVal(key, value, false);  
}  
  
/** Implementation for put and putIfAbsent */  
final V putVal(K key, V value, boolean onlyIfAbsent) { 
    if (key == null || value == null) throw new NullPointerException(); 
    // 对输入的hashcode进行扩散,将输入的hashcode高16位和低16位进行异或操作,将高位的影响扩散到低位(通常我们只关心hashcode的低位),其结果与HASH_BITS进行与操作,确保最高位为0,这是因为在ConcurrentHashMap中,最高位被用作特殊标记
    // 目的是使得散列更加均匀
    int hash = spread(key.hashCode());  
    int binCount = 0;  
    for (Node<K,V>[] tab = table;;) {  
        Node<K,V> f; int n, i, fh;  
        if (tab == null || (n = tab.length) == 0)  
            // 初始化hash表
            tab = initTable();  
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // hash桶tab[i]为空 
            if (casTabAt(tab, i, null,  
                         new Node<K,V>(hash, key, value, null)))  // 通过cas将新的节点添加到桶中,如果添加成功则跳出循环
                break;                   // no lock when adding to empty bin  
        }  
        else if ((fh = f.hash) == MOVED)  // 一个bin(桶)中的节点数超过一定阈值时,会触发扩容操作,扩容时原有的节点会被分散到新的hash表中,为了标记这个过程,原有的接点会被替换为一个特殊的节点,其hash值为MOVED
            // 当前节点的hash值 == MOVED 表明hash正在进行扩容
            tab = helpTransfer(tab, f);  // 将当前节点f中的键值对移动到新的hash表中
        else { // 当前桶中有元素 
            V oldVal = null;  
            synchronized (f) { // 对当前节点f节点加锁
                if (tabAt(tab, i) == f) { // 如果f在桶tab[i]时
                    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; // 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) { // f为树节点 
                        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) { 
                // 树化的条件:桶中节点个数达到8时 && ConcurrentHashMap中的节点数量达到64
                if (binCount >= TREEIFY_THRESHOLD)  
                    treeifyBin(tab, i);  
                if (oldVal != null)  
                    return oldVal;  
                break;  
            }  
        }  
    } 
    // 更新ConcurrentHashMap中元素的数量
    addCount(1L, binCount);  
    return null;  
}
initTable方法
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // sizeCtl即阈值用于表的初始化和扩容
        // -1 正在初始化 
        // < -1 多个线程正在进行调整大小操作,其绝对值为正在调整大小的线程数
        // sizeCtl被声明为volatile,因为在多线程环境下,可能有多个线程同时对sizeCtl进行读写操作,为了保证所有线程都能看到最新的sizeCtl值
        if ((sc = sizeCtl) < 0)
            // 当前线程在尝试初始化table时,发现已经有其他线程进行初始化,所以让出CPU资源
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            // compareAndSwapInt是Unsafe类的方法,是一个原子操作
            // SIZECTL是当前值,sc是期望值,-1是新值
            // 如果SIZECTL == sc 返回true 将这个变量的值设置为新值-1 否则不做任何操作 返回false
            try {
                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;
                    // 阈值等于table长度的3/4倍
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

helpTransfer方法

    // 入参:当前的hash表tab和节点f(ForwardingNode节点)
    // ForwardingNode是一个特殊的节点,不存储任何键值对,而是存储了一个指向新的hash表的引用。当其他线程在进行查询操作时,如果遇到ForwardingNode,则表面ConcurrentHashMap正在进行扩容,通过ForwardingNode中存储的引用找到新的hash表
    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        // tab不为空 && f是ForwardingNode类型 获取到ForwardingNode中存储的新hash表newTab
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
            // nextTab == nextTable && table == tab 表明当前hash表没有变化
            // sizeCtl < 0 表明正在进行扩容操作
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                // 确定是否还需要继续扩容操作
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                    transferIndex <= 0)
                    break;
                // CAS将sc + 1,如果成功则transfer扩容并跳出循环
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }
transfer方法
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride; // stride 步长
        // NCPU是CPU核心数,也就是我们常说的机器配置4核16G中4
        // 如果NCPU>1 则步长 = n / 8 / NCPU 保证多处理器环境下将hash表的部分区域分配给不同的处理器进行处理,以实现并发处理
        // 如果NCPU<=1 则步长 = n
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        // 说明是一个新的扩容操作 创建一个新的hash表,长度是旧hash表的2倍
        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 = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        // 帮助其他线程知道扩容操作正在进行
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        // 控制循环进程 为true时,表示还要继续寻找下一个需要处理的元素的索引
        boolean advance = true;
        // 控制是否完成扩容
        boolean finishing = false; // to ensure sweep before committing nextTab
        // i表示当前正在处理的元素的索引,每次循环开始时,i都会自减1
        // bound是当前处理的元素的边界,只有当i>=bound或者完成扩容时,才会停止寻找下一个元素
        // nextIndex是下一个需要处理的元素索引(等于transferIndex),nextIndex<=0则i=-1并停止寻找下一个元素
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            // 这个while循环主要是为了尽可能利用多核CPU的并行处理能力,从而提升扩容效率
            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))) { // nextIndex>0 尝试使用CAS操作更新transferIndex,并将i和bound设置为新的值。
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) { // 当前索引是否超出边界 或者是否已经完成了扩容
                int sc;
                if (finishing) { // 如果已经完成了扩容 更新table 和 sizeCtl
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1); // 扩容后的阈值 2*n*3/4
                    return;
                }
                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) // i指向的元素为空
                advance = casTabAt(tab, i, null, fwd); // CAS将i指向的元素设置为fwd,并将advance设置为true
            else if ((fh = f.hash) == MOVED) // 当前节点已经被处理过(转移到新hash表)
                advance = true; // already processed
            else {
                synchronized (f) { // 锁住当前节点f
                    if (tabAt(tab, i) == f) { // 确保当前节点没有被其他线程修改
                        Node<K,V> ln, hn;
                        if (fh >= 0) { // 链表
                            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;
                                }
                            }
                            // 如果runBit==0 则节点在新的hash表中的位置是i 否则为i + n 
                            // 和HashMap扩容不同的是,扩容后元素在新hash表的位置,是通过hashcode和新数组长度与运算的
                            // 但是在ConcurrentHashMap,为了支持并发扩容,在扩容的过程中,扩容后元素在新hash表的位置,是通过节点的hash值和旧数组长度与运算的。如果结果为0,节点在新hash表的位置就是i,否则就是i + n,扩容过程可以分两步进行,先将原来的节点复制到新位置i,然后再讲节点复制到为止i + n,这两步操作可以由不同的线程并发执行
                            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)
                                    // 低位 扩容到新表中的i
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    // 高位 扩容到新表中的i + n
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            // 将节点ln插入到nextTab的i位置
                            setTabAt(nextTab, i, ln);
                            // 将节点的hn插入到nextTab的i + n位置
                            setTabAt(nextTab, i + n, hn);
                            // 在旧hash表tab的索引i设置为fwd,fwd是一个ForwardingNode,是一个特殊节点,用于扩容操作中表示当前的节点已经被移动
                            // 扩容过程中,当一个桶的节点被移动到新的hash表后,会在旧hash表对应的位置插入一个ForwardingNode,表示这个位置的节点已经被转移到新的hash表,当其他线程访问旧的hash表时,如果看到一个ForwardingNode,就会去新的hash表查找
                            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) { // 同理 树节点转移到新hash表的i
                                    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;
                        }
                    }
                }
            }
        }
    }

get方法

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) { // 找到对应hash桶
            if ((eh = e.hash) == h) { // 如果是桶中第一个节点
                if ((ek = e.key) == key || (ek != null && key.equals(ek))) // key相等的话,返回val
                    return e.val;
            }
            else if (eh < 0)
                // eh < 0表示当前节点是一个特殊节点 在ConcurrentHashMap中有四种特殊的节点
                // 1. MOVED:表示当前节点是一个ForwardingNode,表示当前节点已经被移动到新hash表中
                // 2. TREEBIN:表示当前节点是一个TreeBin,是红黑树的根节点
                // 3. TreeNode: 表示当前节点是一个红黑树节点
                // 4. RESERVED:表示当前节点已经被其他线程预定,其他线程不能进行操作
                // find() 方法有具体不同的实现类来实现
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) { // 其他情况下当做链表处理,遍历链表,找到对应的key,则返回对应的val
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

find方法
    Node<K,V> find(int h, Object k) { // 这里是find的默认实现方法,遍历链表,找到对应的key,则返回对应的val
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }

未完待续...