Skip to content

集合-ConcurrentHashMap源码解析

一、为什么需要ConcurrentHashMap?

在并发场景下,HashMap(非线程安全)会出现死循环(JDK1.7链表扩容时的循环引用)、数据覆盖等问题;Hashtable(全局synchronized锁)和Collections.synchronizedMap(包装HashMap,全局锁)则因锁粒度太大,导致并发性能低下(同一时间只能有一个线程操作Map)。

ConcurrentHashMap的目标是在保证线程安全的前提下,最大化并发效率,其核心策略是:

  • 减少锁粒度(分段锁→节点锁);
  • 无锁操作(CAS);
  • 高效数据结构(链表→红黑树);
  • 多线程协作(扩容时协助转移节点)。

二、JDK1.7的ConcurrentHashMap:分段锁(Segment)

JDK1.7的ConcurrentHashMap采用Segment数组+HashEntry链表的结构,每个Segment本质是一个小型HashMap,并继承自ReentrantLock(可重入锁)。

1. 核心结构

java
// ConcurrentHashMap的顶级结构:Segment数组
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    final Segment<K,V>[] segments; // 分段数组
    final int segmentMask;         // 分段掩码(用于计算Segment索引)
    final int segmentShift;        // 分段移位(用于计算Segment索引)
    // ...
}

// 每个Segment是一个可重入锁,内部包含HashEntry链表
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile HashEntry<K,V>[] table; // 哈希表(链表数组)
    transient int count;                       // 当前Segment的元素数量
    transient int modCount;                    // 修改次数(用于快速失败,但ConcurrentHashMap迭代是弱一致的)
    transient int threshold;                   // 扩容阈值(table.length * loadFactor)
    final float loadFactor;                    // 负载因子
    // ...
}

// HashEntry:链表节点,存储键值对
static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;          // volatile修饰,保证可见性
    volatile HashEntry<K,V> next; //  volatile修饰,保证链表结构的可见性
    // ...
}

2. 核心操作分析

(1)put操作:分段加锁

put操作的核心是找到对应的Segment,对该Segment加锁,再执行类似HashMap的插入逻辑

java
public V put(K key, V value) {
    if (value == null) throw new NullPointerException();
    int hash = hash(key); // 计算key的hash值
    // 计算Segment索引:(hash >>> segmentShift) & segmentMask
    int segmentIndex = (hash >>> segmentShift) & segmentMask;
    // 获取对应的Segment(若未初始化则初始化)
    Segment<K,V> s = ensureSegment(segmentIndex);
    // 对Segment加锁,执行插入
    return s.put(key, hash, value, false);
}

// Segment的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; // 计算HashEntry链表的索引
        HashEntry<K,V> first = tab[index];   // 链表头节点
        for (HashEntry<K,V> e = first;;) {
            if (e != null) { // 链表非空,遍历查找
                K k = e.key;
                if (e.hash == hash && (k == key || key.equals(k))) { // 找到相同key
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value; // 修改值(volatile保证可见性)
                        modCount++;
                    }
                    break;
                }
                e = e.next;
            } else { // 链表为空,插入新节点
                if (node != null) {
                    node.next = first;
                } else {
                    node = new HashEntry<>(hash, key, value, first);
                }
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY) { // 超过阈值,扩容(Segment内部扩容)
                    rehash(node);
                } else {
                    tab[index] = node; // 插入链表头
                }
                count = c;
                modCount++;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock(); // 释放锁
    }
    return oldValue;
}

关键细节

  • 分段锁:每个Segment独立加锁,不同Segment的操作可以并发执行(比如线程1操作Segment 0,线程2操作Segment 1,互不阻塞);
  • volatile保证可见性HashEntryvaluenextvolatile修饰,确保修改后其他线程能立即看到;
  • 扩容:Segment内部扩容(仅扩容当前Segment的table),不会影响其他Segment。

(2)get操作:无锁

get操作不需要加锁,因为HashEntryvaluenextvolatile的,保证了可见性

java
public V get(Object key) {
    int hash = hash(key);
    // 计算Segment索引,获取对应的Segment
    Segment<K,V> s = segmentForHash(hash);
    // 调用Segment的get方法
    return s == null ? null : s.get(key, hash);
}

// Segment的get方法
final V get(Object key, int hash) {
    if (count == 0) return null; // 快速失败(Segment为空)
    HashEntry<K,V> e = getFirst(hash); // 获取链表头节点
    while (e != null) {
        if (e.hash == hash && (e.key == key || key.equals(e.key))) { // 找到目标节点
            return e.value;
        }
        e = e.next;
    }
    return null;
}

关键细节

  • get操作无锁,因为volatile保证了value的可见性,且链表结构的修改(next)也会被立即看到;
  • 弱一致性:迭代时若有其他线程修改Map,迭代器不会抛出ConcurrentModificationException,但会反映最新状态(因为nextvolatile的)。

(3)size计算:分段累加

size操作需要累加所有Segment的count值,但并发场景下count可能随时变化,因此需要重试机制

java
public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // 是否溢出
    long sum;         // 总元素数
    long last = 0L;   // 上一次的总和
    int retries = -1; // 重试次数(初始为-1,表示第一次尝试)
    for (;;) {
        if (retries++ == RETRIES_BEFORE_LOCK) { // 重试超过3次,对所有Segment加锁
            for (int i = 0; i < segments.length; i++) {
                segments[i].lock();
            }
        }
        sum = 0L;
        overflow = false;
        for (int i = 0; i < segments.length; i++) {
            sum += segments[i].count; // 累加每个Segment的count
        }
        if (sum == last) { // 总和不变,返回(说明并发修改停止)
            break;
        }
        last = sum;
        if (retries > RETRIES_BEFORE_LOCK) { // 已加锁,直接返回
            break;
        }
    }
    if (overflow || sum > Integer.MAX_VALUE) {
        return Integer.MAX_VALUE;
    } else {
        return (int)sum;
    }
}

关键细节

  • 先尝试无锁累加(重试3次),若总和不变则返回;
  • 若重试失败,对所有Segment加锁(全局锁),确保累加的准确性(此时并发性能极低,但size操作不是高频操作)。

3. JDK1.7的优缺点

  • 优点:分段锁机制提高了并发度(并发度=Segment数量,默认16);
  • 缺点
    • 结构复杂(Segment+HashEntry);
    • 扩容成本高(Segment内部扩容,无法跨Segment协作);
    • 链表查询效率低(长度过长时,查询时间复杂度为O(n))。

三、JDK1.8的ConcurrentHashMap:数组+链表+红黑树

JDK1.8彻底重构了ConcurrentHashMap的结构,去掉了Segment,采用Node数组+链表+红黑树的结构,并用**CAS+synchronized**替代了分段锁,进一步优化了并发性能。

1. 核心结构

java
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    transient volatile Node<K,V>[] table; // 哈希表(Node数组,默认容量16)
    transient volatile Node<K,V>[] nextTable; // 扩容时的临时新表
    transient volatile long baseCount; // 基础计数器(无竞争时使用)
    transient volatile int sizeCtl; // 控制标志(-1:正在初始化;-N:有N个线程正在扩容;正数:下一次扩容的阈值)
    // ...
}

// 普通Node节点(链表节点)
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;          // volatile修饰,保证可见性
    volatile Node<K,V> next; //  volatile修饰,保证链表结构的可见性
    // ...
}

// 红黑树的根节点(包装TreeNode)
static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;       // 红黑树的根
    volatile TreeNode<K,V> first; // 链表的头节点(红黑树转为链表时使用)
    volatile Thread waiter;   // 等待锁的线程(用于computeIfAbsent等方法)
    // ...
}

// 红黑树节点
static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;     // 父节点
    TreeNode<K,V> left;       // 左子节点
    TreeNode<K,V> right;      // 右子节点
    TreeNode<K,V> prev;       // 链表前驱(用于红黑树转为链表)
    boolean red;              // 是否为红节点
    // ...
}

// 扩容标记节点(ForwardingNode):旧表中的节点转移到新表后,旧表的位置会设置为该节点
static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable; // 新表
    // ...
}

2. 核心属性解析

  • table:主哈希表(Node数组),默认容量16,扩容时变为2倍;
  • nextTable:扩容时的临时新表(容量为旧表的2倍);
  • sizeCtl:控制哈希表的初始化和扩容:
    • sizeCtl = -1:正在初始化;
    • sizeCtl = -N:有N个线程正在扩容;
    • sizeCtl > 0:下一次扩容的阈值(table.length * loadFactor,默认0.75);
    • sizeCtl = 0:未初始化(默认值);
  • baseCount:无竞争时的元素计数器(用volatile修饰);
  • counterCells:竞争时的元素计数器(数组,每个元素是CounterCell,用volatile修饰,减少竞争)。

3. 核心操作分析

(1)初始化:initTable()

ConcurrentHashMap的初始化是懒加载的(第一次put时触发),用CAS保证线程安全。

java
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0) { // 正在初始化,让出CPU
            Thread.yield();
        } else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // CAS设置sizeCtl为-1(标记正在初始化)
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 默认容量16
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2); // 计算阈值(n * 0.75)
                }
            } finally {
                sizeCtl = sc; // 设置阈值
            }
            break;
        }
    }
    return tab;
}

关键细节

  • CAS设置sizeCtl为-1,防止多个线程同时初始化;
  • 初始化后,sizeCtl设置为阈值(容量*0.75)。

(2)put操作:putVal()

put操作是ConcurrentHashMap的核心,逻辑较复杂,需处理初始化、无锁插入、加锁插入、扩容协助、红黑树转换等场景。

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

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException(); // 不允许null键/值
    int hash = spread(key.hashCode()); // 计算hash(优化分散性)
    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(); // 初始化table
        } else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 当前位置为空,用CAS插入新节点
            if (casTabAt(tab, i, null, new Node<>(hash, key, value, null))) {
                break; // 插入成功,退出循环
            }
        } else if ((fh = f.hash) == MOVED) { // 当前位置是ForwardingNode(正在扩容),帮助扩容
            tab = helpTransfer(tab, f);
        } else { // 当前位置有节点,加锁插入(synchronized锁链表头节点)
            V oldVal = null;
            synchronized (f) { // 锁链表头节点(或红黑树根节点)
                if (tabAt(tab, i) == f) { // 再次检查(防止并发修改)
                    if (fh >= 0) { // 链表节点(hash>=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)))) { // 找到相同key
                                oldVal = e.val;
                                if (!onlyIfAbsent) {
                                    e.val = value; // 修改值(volatile保证可见性)
                                }
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) { // 到达链表尾部,插入新节点
                                pred.next = new Node<>(hash, key, value, null);
                                break;
                            }
                        }
                    } else if (f instanceof TreeBin) { // 红黑树节点(hash<0,因为TreeBin的hash是-2)
                        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) { // 链表长度超过8,转红黑树
                    treeifyBin(tab, i);
                }
                if (oldVal != null) {
                    return oldVal; // 返回旧值
                }
                break;
            }
        }
    }
    addCount(1L, binCount); // 更新元素数量
    return null;
}

关键细节

  • hash计算:用spread方法优化hash的分散性((h ^ (h >>> 16)) & 0x7fffffff),减少冲突;
  • 无锁插入:当前位置为空时,用CAS插入新节点(casTabAt方法,底层调用UnsafecompareAndSwapObject);
  • 扩容协助:若当前位置是ForwardingNodehash == MOVED,MOVED=-1),则调用helpTransfer方法帮助扩容(多线程协作扩容);
  • 加锁插入:当前位置有节点时,对链表头节点(或红黑树根节点)加synchronized锁,防止并发修改;
  • 红黑树转换:链表长度超过TREEIFY_THRESHOLD(8)时,调用treeifyBin方法转红黑树(若table容量小于MIN_TREEIFY_CAPACITY(64),则先扩容);
  • 更新数量:调用addCount方法更新元素数量(用baseCountcounterCells实现,减少竞争)。

(3)扩容操作:transfer()(多线程协作)

扩容是ConcurrentHashMap的难点,JDK1.8采用多线程协作扩容,提高扩容速度。扩容的触发条件是:size > sizeCtl(元素数量超过阈值)。

java
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // 计算每个线程处理的桶数(stride):CPU核心数越多,stride越大(最小16)
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) {
        stride = MIN_TRANSFER_STRIDE; // MIN_TRANSFER_STRIDE=16
    }
    if (nextTab == null) { // 初始化新表(容量为旧表的2倍)
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {
            sizeCtl = Integer.MAX_VALUE; // 扩容失败,设置sizeCtl为最大值(禁止后续扩容)
            return;
        }
        nextTable = nextTab;
        transferIndex = n; // 转移索引(从旧表的最后一个桶开始)
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<>(nextTab); // 扩容标记节点(hash=MOVED)
    boolean advance = true; // 是否需要推进转移索引
    boolean finishing = false; // 是否完成扩容
    for (int i = 0, bound = 0;;) { // 循环处理每个桶
        Node<K,V> f; int fh;
        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 = Math.max(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 >>> 2); // 更新阈值(新容量*0.75)
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // 减少正在扩容的线程数
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) { // 检查是否还有线程在扩容
                    return;
                }
                finishing = advance = true;
                i = n; // 重新遍历旧表,检查是否有遗漏的节点
            }
        } else if ((f = tabAt(tab, i)) == null) { // 旧表的当前桶为空,设置为ForwardingNode
            advance = casTabAt(tab, i, null, fwd);
        } else if ((fh = f.hash) == MOVED) { // 旧表的当前桶已处理(ForwardingNode),跳过
            advance = true;
        } else { // 旧表的当前桶有节点,加锁转移(synchronized锁桶的头节点)
            synchronized (f) {
                if (tabAt(tab, i) == f) { // 再次检查(防止并发修改)
                    Node<K,V> ln, hn; // ln:低位链表(hash & n == 0);hn:高位链表(hash & n != 0)
                    if (fh >= 0) { // 链表节点
                        int runBit = fh & n; // 计算当前节点的高位bit(0或n)
                        Node<K,V> lastRun = f;
                        // 找到最后一个连续的runBit节点(优化转移效率)
                        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<>(ph, pk, pv, ln);
                            } else {
                                hn = new Node<>(ph, pk, pv, hn);
                            }
                        }
                        // 将低位链表放入新表的i位置,高位链表放入i+n位置
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 旧表的i位置设置为ForwardingNode(标记已处理)
                        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;
                            if ((h & n) == 0) {
                                if (loTail == null) {
                                    lo = (TreeNode<K,V>)e;
                                } else {
                                    loTail.next = (TreeNode<K,V>)e;
                                }
                                loTail = (TreeNode<K,V>)e;
                                lc++;
                            } else {
                                if (hiTail == null) {
                                    hi = (TreeNode<K,V>)e;
                                } else {
                                    hiTail.next = (TreeNode<K,V>)e;
                                }
                                hiTail = (TreeNode<K,V>)e;
                                hc++;
                            }
                        }
                        // 将低位链表转为红黑树或链表(若节点数<=6,转链表)
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (lc != 0) ? new TreeBin<>(lo) : null;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (hc != 0) ? new TreeBin<>(hi) : null;
                        // 将低位和高位节点放入新表
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 旧表的i位置设置为ForwardingNode
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

关键细节

  • 多线程协作:每个线程处理stride个桶(默认16),通过transferIndex(转移索引)分配任务(从旧表的最后一个桶开始,往前处理);
  • 扩容标记:旧表中的桶处理完成后,设置为ForwardingNodehash=MOVED),其他线程访问该桶时,会发现是ForwardingNode,从而调用helpTransfer方法帮助扩容;
  • 节点转移:将旧表中的节点分为低位链表hash & 旧容量 == 0)和高位链表hash & 旧容量 != 0),分别放入新表的ii+旧容量位置;
  • 红黑树处理:若红黑树的节点数<=UNTREEIFY_THRESHOLD(6),则转为链表(减少红黑树的维护成本)。

(4)get操作:无锁

get操作不需要加锁,因为Nodevaluenextvolatile的,保证了可见性

java
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int hash = spread(key.hashCode()); // 计算hash
    if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & hash)) != null) {
        if ((eh = e.hash) == hash) { // 当前节点是目标节点(hash相等)
            if ((ek = e.key) == key || (ek != null && key.equals(ek))) {
                return e.val;
            }
        } else if (eh < 0) { // 当前节点是红黑树或ForwardingNode(hash<0)
            return (p = e.find(hash, key)) != null ? p.val : null;
        }
        // 遍历链表,查找目标节点
        while ((e = e.next) != null) {
            if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                return e.val;
            }
        }
    }
    return null;
}

关键细节

  • 快速查找:先检查当前桶的头节点是否是目标节点(hash相等且key相等);
  • 红黑树查找:若当前节点是TreeBinhash=-2),调用find方法遍历红黑树(时间复杂度O(log n));
  • ForwardingNode处理:若当前节点是ForwardingNodehash=-1),则通过nextTable(新表)查找目标节点(扩容时的处理)。

(5)size计算:sumCount()

JDK1.8的size计算采用**baseCount+counterCells**的方式,减少竞争。

java
public int size() {
    long n = sumCount();
    return (n < 0) ? 0 : (n > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n;
}

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount; // 累加基础计数器
    if (as != null) {
        for (int i = 0; i < as.length; i++) { // 累加counterCells数组中的值
            if ((a = as[i]) != null) {
                sum += a.value;
            }
        }
    }
    return sum;
}

// CounterCell:竞争时的计数器(用volatile修饰)
static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

关键细节

  • 无竞争时:用baseCountvolatile)记录元素数量(addCount方法直接修改baseCount);
  • 有竞争时:用counterCells数组(每个元素是CounterCell)记录元素数量(addCount方法通过CAS修改counterCells中的元素,减少竞争);
  • sumCount:累加baseCountcounterCells中的值,得到总元素数量(弱一致性,因为并发修改时counterCells可能变化,但size操作不是高频操作)。

4. JDK1.8的优缺点

  • 优点
    • 锁粒度更小(节点锁,而非Segment锁);
    • 无锁操作(CAS)提高了并发效率;
    • 红黑树优化了链表的查询效率(O(n)→O(log n));
    • 多线程协作扩容提高了扩容速度;
    • 结构更简单(去掉了Segment);
  • 缺点
    • synchronized的性能虽然优化了(偏向锁、轻量级锁),但在极高并发下,仍可能比ReentrantLock略低(但实际场景中差异很小);
    • 红黑树的维护成本较高(插入、删除时需要调整结构)。

四、JDK1.7与JDK1.8的对比

维度JDK1.7JDK1.8
结构Segment数组+HashEntry链表Node数组+链表+红黑树
锁机制分段锁(ReentrantLock)CAS+synchronized(节点锁)
扩容Segment内部扩容(单线程)多线程协作扩容(全局扩容)
查询效率链表(O(n))链表(O(n))/红黑树(O(log n))
并发度Segment数量(默认16)节点数量(理论上无限)
null键/值不允许不允许

五、ConcurrentHashMap的适用场景

  • 高并发缓存:比如缓存用户信息、商品信息等,需要频繁读取和写入;
  • 计数器:比如统计接口调用次数、用户点击次数等,需要原子性的increment操作;
  • 并发集合:比如多线程处理任务时,存储中间结果。

六、注意事项

  • 不允许null键/值ConcurrentHashMap不允许null键或null值(否则抛出NullPointerException),而HashMap允许;
  • 弱一致性:迭代器(Iterator)是弱一致的,不会抛出ConcurrentModificationException,但会反映迭代开始后的最新状态;
  • 扩容时的性能:扩容时,多线程协作会提高扩容速度,但仍会占用一定的CPU资源(避免在高并发场景下频繁扩容,建议初始化时设置合适的容量);
  • computeIfAbsent方法:该方法用于原子性地计算并插入值(若key不存在),内部用ReservationNode(占位节点)防止多个线程同时计算同一个key的值(避免重复计算)。

七、总结

ConcurrentHashMap是Java并发包中最常用的并发集合,其设计核心是在保证线程安全的前提下,最大化并发效率。JDK1.8的重构(去掉Segment、用CAS+synchronized、红黑树)进一步优化了性能,使其成为高并发场景下的首选。

理解ConcurrentHashMap的关键是:

  • 锁粒度的优化(从Segment到Node);
  • 无锁操作的应用(CAS);
  • 高效数据结构的使用(红黑树);
  • 多线程协作的设计(扩容时协助转移节点)。

通过深入剖析其源码,可以更好地理解并发编程中的锁优化无锁算法数据结构设计等核心思想,为编写高效的并发程序打下基础。