源码角度分析下ConcurrentHashMap是如何实现线程安全的?

ConcurrentHashMap 是 Java 并发包 (java.util.concurrent) 中提供的一个线程安全的哈希表实现。它通过多种并发控制机制来实现高效的线程安全操作。以下从源码角度分析 ConcurrentHashMap 如何实现线程安全。

1. 数据结构

ConcurrentHashMap 的底层数据结构是一个 数组 + 链表 + 红黑树 的组合。与 HashMap 类似,但在并发控制上做了优化。
核心数据结构:
Node:链表节点,存储键值对。
TreeNode:红黑树节点,当链表长度超过阈值时转换为红黑树。
Table:一个 volatile 修饰的 Node[] 数组,存储桶(Bucket)。

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

2. 线程安全的实现机制

ConcurrentHashMap 通过以下几种机制实现线程安全:

(1)分段锁(JDK 1.7)

在 JDK 1.7 中,ConcurrentHashMap 使用分段锁(Segment)来实现线程安全。它将整个哈希表分成多个段(Segment),每个段独立加锁,不同段之间可以并发操作。

Segment:继承自 ReentrantLock,每个段是一个独立的哈希表。
锁粒度:锁的粒度从整个表缩小到段,提高了并发性能。

(2)CAS + synchronized(JDK 1.8 及以后)

在 JDK 1.8 中,ConcurrentHashMap 抛弃了分段锁,改为使用 CAS(Compare-And-Swap) 和 synchronized 来实现线程安全。

CAS:用于无锁化的更新操作(如插入、替换)。
synchronized:用于锁定单个桶(链表或红黑树的头节点)。

3. 核心源码分析(JDK 1.8)

以下从 JDK 1.8 的源码角度分析 ConcurrentHashMap 的线程安全实现。

(1)插入操作(put 方法)

插入操作的核心逻辑在 putVal 方法中。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    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;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // 初始化表
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 如果桶为空,使用 CAS 插入新节点,如果这里失败就不会break,会循环一次,下一次就无法进入该分支
            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 (f) { // 锁定桶的头节点
            //这里比较关键,他只锁了一个节点的对象,如果你并发操作其他对象,理论上也可以获取到锁
                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;
}

CAS 插入:如果桶为空,使用 casTabAt 方法通过 CAS 插入新节点。

锁桶头节点:如果桶不为空,使用 synchronized 锁定桶的头节点,然后遍历链表或红黑树进行插入。

(2)读取操作(get 方法)

读取操作是无锁的,直接通过 volatile 关键字保证可见性。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
        //其他代码
}   
 
transient volatile Node<K,V>[] table;

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) { // 获取桶的头节点
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val; // 直接返回
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null; // 红黑树查找
        while ((e = e.next) != null) { // 链表查找
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

volatile 保证可见性:table 和 Node 的 val 字段都是 volatile 的,确保读取操作能够看到最新的值。

(3)扩容机制

ConcurrentHashMap 的扩容是通过多线程协作完成的。
transfer 方法:将旧表的元素迁移到新表。
helpTransfer 方法:其他线程协助扩容。

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

4. 总结

ConcurrentHashMap 通过以下机制实现线程安全:
CAS:用于无锁化的更新操作。
synchronized:锁定单个桶的头节点,减少锁粒度。
volatile:保证变量的可见性。
多线程协作扩容:提高扩容效率。

相比于 Hashtable 和 Collections.synchronizedMap,ConcurrentHashMap 在并发性能上有显著优势,适合高并发场景。

你可能感兴趣的:(重拾java,java基础知识,安全,哈希算法,算法)