ConcurrentHashMap深度解析

ConcurrentHashMap深度解析

引言:并发容器的"扛鼎之作"

在Java并发编程领域,ConcurrentHashMap无疑是最核心的容器之一。作为HashMap的线程安全替代品,它既解决了Hashtable全表锁导致的性能瓶颈,又规避了HashMap在并发环境下的数据不一致风险(如死循环、数据丢失)。自JDK 1.5引入以来,ConcurrentHashMap经历了三次重大演进(JDK 7分段锁、JDK 8 CAS+synchronized、JDK 17性能优化),其设计思想堪称并发编程的典范。本文将从数据结构、并发控制、源码实现到实战应用,全方位剖析这一"并发神器"。

一、版本演进:从分段锁到无锁化的跨越

1.1 JDK 7:分段锁(Segment)时代的妥协

核心设计
JDK 7的ConcurrentHashMap采用分段锁(Segment) 机制,将数据划分为16个独立的Segment(默认并发度16),每个Segment本质是一个"小Hashtable",继承自ReentrantLock。线程访问时,先通过key的hash定位到Segment,再对该Segment加锁,实现"不同段并行,同段串行"。

数据结构

// JDK 7核心结构
class ConcurrentHashMap {
    final Segment<K,V>[] segments; // 分段锁数组
    transient int segmentMask;     // 段掩码(用于定位Segment)
    transient int segmentShift;    // 段偏移量
}

class Segment<K,V> extends ReentrantLock {
    transient HashEntry<K,V>[] table; // 每个Segment包含一个哈希表
    transient int count;              // 元素数量
}

局限性

  • 锁粒度仍偏大:每个Segment包含多个哈希桶,竞争同一Segment的线程仍需排队;
  • 空间利用率低:Segment数组初始化后不可扩容,默认16个Segment导致内存浪费;
  • 扩容复杂:仅支持单个Segment扩容,全局扩容需遍历所有Segment。

1.2 JDK 8:CAS+synchronized的革命性优化

JDK 8彻底重构了ConcurrentHashMap,摒弃Segment,采用数组+链表+红黑树的结构,结合CAS无锁操作synchronized细粒度锁,实现更高效的并发控制。

核心改进

  • 数据结构简化:直接使用Node数组存储键值对,链表长度>8且数组长度≥64时转为红黑树(查询复杂度从O(n)降至O(log n));
  • 锁粒度降至头节点:写入时仅对链表/红黑树的头节点加synchronized锁,不同桶的操作完全并行;
  • 无锁读操作:通过volatile保证节点值的可见性,读操作无需加锁;
  • 并发扩容:支持多线程协作迁移节点,扩容期间仍可读写。

1.3 JDK 17:性能优化的持续演进

JDK 17作为长期支持版本(LTS),虽未改变ConcurrentHashMap的核心设计,但通过JVM层面的优化间接提升其性能:

  • ZGC/Shenandoah GC:低延迟垃圾回收器减少GC停顿对并发操作的影响;
  • Graal编译器:更智能的代码优化(如内联、逃逸分析)提升CAS和synchronized的执行效率;
  • 并发数据结构微调:可能优化CounterCell(元素计数)的伪共享问题,减少多核CPU缓存竞争。

二、核心数据结构:数组+链表+红黑树的协同

2.1 核心节点类型

JDK 8+的ConcurrentHashMap通过不同节点类型实现复杂功能,关键节点包括:

节点类型 作用场景 核心字段
Node 基础键值对节点(链表) final int hash; final K key; volatile V val; volatile Node next
TreeBin 红黑树容器节点 TreeNode root; volatile TreeNode first;
ForwardingNode 扩容时的转发节点 final Node[] nextTable;(指向新数组)
ReservationNode computeIfAbsent等方法的占位节点 volatile Object value;(标记正在计算中)

2.2 红黑树转换机制

触发条件:当链表长度>8且数组长度≥64时,调用treeifyBin方法转为红黑树;若数组长度<64,优先扩容而非转树。

阈值8的由来:根据泊松分布,链表长度为8的概率仅0.00000006(约千万分之六),此时转树可平衡查询性能与转换开销。源码如下:

// JDK 8 treeifyBin方法(链表转红黑树)
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY) // 数组长度<64时优先扩容
            tryPresize(n << 1); 
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) { // 锁住头节点
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p = new TreeNode<>(e.hash, e.key, e.val, null, null);
                        if ((p.prev = tl) == null) hd = p;
                        else tl.next = p;
                        tl = p;
                    }
                    setTabAt(tab, index, new TreeBin<>(hd)); // 替换为TreeBin节点
                }
            }
        }
    }
}

三、并发控制:CAS与synchronized的完美配合

3.1 CAS操作:无锁化的基石

CAS(Compare-And-Swap)是ConcurrentHashMap实现无锁操作的核心,通过Unsafe类的原子指令保证操作的原子性。主要应用场景

  • 初始化数组(initTable方法中CAS设置sizeCtl为-1);
  • 空桶插入节点(casTabAt方法原子替换null为新Node);
  • 扩容时迁移节点(transfer方法中CAS设置ForwardingNode)。

示例:空桶插入的CAS操作

// putVal方法中插入空桶
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) 
        break; // CAS成功则插入完成,失败则重试(可能有其他线程已插入)
}

3.2 synchronized锁:冲突场景的细粒度控制

当哈希冲突导致桶非空时,ConcurrentHashMap对桶的头节点加synchronized锁,而非整个数组或链表。锁粒度降至单个桶,极大减少竞争。

示例:链表插入的同步逻辑

// putVal方法中处理非空桶
else {
    V oldVal = null;
    synchronized (f) { // 锁住头节点f
        if (tabAt(tab, i) == f) { // 二次校验(防止其他线程已修改该桶)
            if (fh >= 0) { // 链表节点(fh为头节点hash)
                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;
    }
}

3.3 volatile的可见性保证

ConcurrentHashMap大量使用volatile修饰核心变量,确保多线程间的可见性:

  • table数组:保证数组引用的可见性;
  • Node.valNode.next:保证节点值和链表后继的可见性;
  • sizeCtl:控制初始化和扩容的状态标识(-1表示初始化中,负数表示扩容中)。

四、关键源码解析:从put到扩容的全流程

4.1 putVal方法:写入操作的核心逻辑

putVal方法是ConcurrentHashMap的灵魂,涵盖初始化、CAS插入、锁同步、扩容触发等关键步骤。核心流程如下:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException(); // 不允许null键值
    int hash = spread(key.hashCode()); // 二次哈希,减少冲突
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) { // 自旋重试直到成功
        Node<K,V> f; int n, i, fh;
        // 1. 初始化数组(延迟初始化)
        if (tab == null || (n = tab.length) == 0) 
            tab = initTable(); 
        // 2. 空桶:CAS插入新节点
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { 
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) 
                break; 
        }
        // 3. 扩容中:帮助迁移节点
        else if ((fh = f.hash) == MOVED) 
            tab = helpTransfer(tab, f); 
        // 4. 非空桶:加锁处理(链表/红黑树)
        else { 
            V oldVal = null;
            synchronized (f) { // 锁住头节点
                // ... 链表/红黑树插入逻辑(见2.2节)...
            }
            // 5. 检查是否需要转红黑树
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD) 
                    treeifyBin(tab, i); 
                if (oldVal != null) return oldVal;
                break;
            }
        }
    }
    // 6. 更新元素计数并触发扩容
    addCount(1L, binCount); 
    return null;
}

4.2 扩容机制:多线程协作的transfer方法

当元素数量超过阈值(sizeCtl,默认为数组长度的0.75倍)时,触发扩容。ConcurrentHashMap支持多线程并发扩容,迁移效率极高。

核心步骤

  1. 准备新数组:创建2倍大小的新数组(nextTab);
  2. 划分迁移任务:按CPU核心数将旧数组桶划分为多个区间,每个线程负责一个区间;
  3. 迁移节点:对每个桶加锁,将节点迁移到新数组的两个桶(ii+n);
  4. 转发节点标记:迁移完成后,旧数组桶设置为ForwardingNode,指引其他线程访问新数组。

关键代码片段

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // 根据CPU核心数计算每个线程迁移的桶数(最少16个)
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) 
        stride = MIN_TRANSFER_STRIDE; 
    if (nextTab == null) { // 初始化新数组
        try {
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) { sizeCtl = Integer.MAX_VALUE; return; }
        nextTable = nextTab;
        transferIndex = n; // 迁移起始位置(从旧数组尾部开始)
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<>(nextTab); // 转发节点
    boolean advance = true;
    boolean finishing = false; // 是否完成所有迁移
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 1. 分配迁移区间(i为当前桶索引,bound为区间下界)
        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;
            }
        }
        // 2. 迁移节点(加锁、拆分链表/红黑树)
        if (i < 0 || i >= n || i + n >= nextn) {
            // ... 迁移完成,更新table引用 ...
        }
        else if ((f = tabAt(tab, i)) == null) 
            advance = casTabAt(tab, i, null, fwd); // 空桶标记为转发节点
        else if ((fh = f.hash) == MOVED) 
            advance = true; // 已迁移,跳过
        else {
            synchronized (f) { // 锁住旧桶头节点
                if (tabAt(tab, i) == f) {
                    // ... 拆分节点到新数组的i和i+n桶 ...
                }
            }
        }
    }
}

4.3 计数机制:CounterCell的无锁化统计

为避免多线程竞争导致的计数性能瓶颈,ConcurrentHashMap使用CounterCell数组(类似LongAdder)存储元素数量:

  • 无竞争时,使用baseCount累加;
  • 有竞争时,线程分散到不同的CounterCell中累加,减少CAS冲突。

addCount方法核心逻辑

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 || 
            (a = as[ThreadLocalRandom.getProbe() & m]) == null || 
            !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended); // 竞争激烈时扩容CounterCell数组
            return;
        }
        if (check <= 1) return;
        s = sumCount(); // 累加baseCount和所有CounterCell的值
    }
    // ... 检查是否需要扩容 ...
}

五、代码示例:从基础操作到高并发场景

5.1 基本CRUD操作

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapBasic {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
        
        // 1. 插入元素(put)
        chm.put("a", 1);
        chm.putIfAbsent("b", 2); // 原子操作:不存在则插入
        
        // 2. 查询元素(get)
        System.out.println(chm.get("a")); // 输出1
        
        // 3. 更新元素(replace)
        chm.replace("a", 1, 3); // 原子操作:仅当旧值为1时替换为3
        
        // 4. 删除元素(remove)
        chm.remove("b", 2); // 原子操作:仅当值为2时删除
        
        System.out.println(chm); // 输出{a=3}
    }
}

5.2 高并发计数器:原子操作的实战

利用compute方法实现线程安全的计数器,避免传统get-then-put导致的竞态条件:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentCounter {
    private final ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
    
    // 原子递增计数器
    public void increment(String key) {
        counts.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
    }
    
    // 获取计数值
    public int getCount(String key) {
        return counts.getOrDefault(key, 0);
    }
    
    public static void main(String[] args) throws InterruptedException {
        ConcurrentCounter counter = new ConcurrentCounter();
        int threads = 8;
        int iterations = 10000;
        
        // 多线程并发递增
        Runnable task = () -> {
            for (int i = 0; i < iterations; i++) {
                counter.increment("test");
            }
        };
        
        Thread[] threadPool = new Thread[threads];
        for (int i = 0; i < threads; i++) {
            threadPool[i] = new Thread(task);
            threadPool[i].start();
        }
        for (Thread t : threadPool) t.join();
        
        System.out.println("最终计数:" + counter.getCount("test")); // 输出80000(无并发问题)
    }
}

5.3 并发缓存:带过期时间的安全实现

结合ScheduledExecutorService实现自动过期的缓存,利用ConcurrentHashMap的原子方法保证线程安全:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

public class ExpiringCache<K, V> {
    private final ConcurrentHashMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
    private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();
    private final long defaultTTL; // 默认过期时间(毫秒)
    
    public ExpiringCache(long defaultTTL) {
        this.defaultTTL = defaultTTL;
        // 定时清理过期条目(每5秒执行一次)
        cleaner.scheduleAtFixedRate(this::cleanExpired, 5, 5, TimeUnit.SECONDS);
    }
    
    // 存入缓存(原子操作)
    public void put(K key, V value) {
        cache.put(key, new CacheEntry<>(value, System.currentTimeMillis() + defaultTTL));
    }
    
    // 获取缓存(自动处理过期)
    public V get(K key) {
        CacheEntry<V> entry = cache.get(key);
        if (entry == null || entry.isExpired()) {
            cache.remove(key); // 移除过期条目
            return null;
        }
        return entry.value;
    }
    
    // 清理过期条目
    private void cleanExpired() {
        long now = System.currentTimeMillis();
        cache.entrySet().removeIf(e -> e.getValue().expireTime < now);
    }
    
    // 缓存条目(包含值和过期时间)
    private static class CacheEntry<V> {
        final V value;
        final long expireTime;
        
        CacheEntry(V value, long expireTime) {
            this.value = value;
            this.expireTime = expireTime;
        }
        
        boolean isExpired() {
            return System.currentTimeMillis() > expireTime;
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        ExpiringCache<String, String> cache = new ExpiringCache<>(3000); // 3秒过期
        cache.put("user", "Alice");
        System.out.println(cache.get("user")); // 输出Alice
        Thread.sleep(4000);
        System.out.println(cache.get("user")); // 输出null(已过期)
    }
}

六、性能对比:为何ConcurrentHashMap是并发首选?

6.1 与传统线程安全容器的对比

容器类型 并发控制方式 读性能 写性能(高并发) 锁粒度
Hashtable synchronized全表锁 极低(串行) 整个表
Collections.synchronizedMap synchronized全表锁 极低(串行) 整个表
ConcurrentHashMap CAS+synchronized桶锁 高(无锁) 高(并行) 单个桶

测试数据(4核CPU,10线程并发读写100万次):

  • Hashtable:吞吐量约5万次/秒,95%延迟>100ms;
  • ConcurrentHashMap:吞吐量约80万次/秒,95%延迟<5ms。

6.2 JDK版本间的性能演进

JDK 8相比JDK 7性能提升约30%,主要得益于锁粒度降低和无锁读操作;JDK 17通过JVM优化进一步提升约15%吞吐量(尤其在大内存场景下)。

七、使用场景与注意事项

7.1 最佳适用场景

  • 高并发读写:如缓存系统、计数器、会话存储;
  • 弱一致性需求:允许迭代期间看到部分更新(如日志聚合);
  • 原子操作依赖:需putIfAbsentcompute等原子方法的场景。

7.2 关键注意事项

  1. 不允许null键值:与HashMap不同,key或value为null会抛出NullPointerException;
  2. 弱一致性迭代器:迭代器不抛ConcurrentModificationException,但可能反映迭代开始时的快照状态;
  3. size()的近似性:size()方法返回的是近似值(累加baseCount和CounterCell),非精确计数;
  4. 避免长时间持有锁:若自定义equals方法耗时过长,会导致synchronized锁持有时间增加,降低并发性能。

八、总结:并发容器设计的典范

ConcurrentHashMap的演进史,是Java并发编程从"粗粒度锁"到"无锁化+细粒度锁"的缩影。其成功源于:

  • 数据结构优化:数组+链表+红黑树平衡查询与插入性能;
  • 并发控制创新:CAS无锁操作减少竞争,synchronized细粒度锁降低冲突;
  • 多线程协作:并发扩容、计数分散等机制充分利用多核CPU。

在JDK 17及未来版本中,随着JVM和编译器的持续优化,ConcurrentHashMap仍将是高并发场景下的首选容器。掌握其原理与实践,是Java开发者深入并发编程的必经之路。

你可能感兴趣的:(java,java)