08-Java并发容器源码剖析:ConcurrentHashMap与CopyOnWriteArrayList

Java并发容器源码剖析:ConcurrentHashMap与CopyOnWriteArrayList

一、并发容器概览与选型

1. JDK并发容器体系

graph TD A[并发容器] --> B[Map体系] A --> C[List体系] A --> D[Queue体系] B --> B1[ConcurrentHashMap] B --> B2[ConcurrentSkipListMap] C --> C1[CopyOnWriteArrayList] C --> C2[ConcurrentLinkedQueue] D --> D1[BlockingQueue] D --> D2[TransferQueue]

2. 容器特性对比

容器类型 线程安全实现 读性能 写性能 适用场景
ConcurrentHashMap CAS + synchronized分段锁 O(1) O(1) 高频读写K-V存储
CopyOnWriteArrayList 写时复制(ReentrantLock) O(1) O(n) 读多写少(白名单/黑名单)
Collections.synchronizedList 方法级synchronized O(1) O(1) 兼容旧代码(不推荐新用)

二、ConcurrentHashMap深度解析

1. JDK7 vs JDK8实现对比

版本 数据结构 锁粒度 哈希冲突解决 并发度控制
JDK7 Segment数组+链表 Segment级别锁 拉链法 构造时固定(不可扩容)
JDK8 Node数组+链表/红黑树 桶级别锁(头节点) 链表转红黑树 动态扩容

2. JDK8核心源码剖析

2.1 putVal()关键流程
java 复制代码
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 1. 计算hash(扰动函数优化)
    int hash = spread(key.hashCode());
    int binCount = 0;
    
    // 2. 自旋插入节点
    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) {
            // 3. CAS尝试插入新节点(无锁优化)
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;                   
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f); // 协助扩容
        else {
            // 4. 锁住桶头节点处理冲突
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { // 链表处理
                        // ...遍历链表更新或插入
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, i); // 转红黑树
                    }
                    else if (f instanceof TreeBin) { // 红黑树处理
                        // ...调用树节点插入方法
                    }
                }
            }
        }
    }
    // 5. 检查扩容
    addCount(1L, binCount);
    return null;
}
2.2 扩容机制(transfer())
  • 多线程协同 :通过sizeCtl标记扩容状态,其他线程检测到后会协助迁移数据
  • 桶迁移策略:每个线程负责一定范围的桶,迁移时按链表/树结构拆分

3. 性能优化技巧

  • 避免长链表:确保key的hashCode()分布均匀
  • 设置初始容量 :减少扩容次数,建议预计元素数/负载因子 + 1
  • 并行计算 :使用forEach()/reduce()等并行方法

三、CopyOnWriteArrayList实现原理

1. 核心设计思想

java 复制代码
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 1. 复制新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 2. 写入新元素
        newElements[len] = e;
        // 3. 替换引用
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

2. 读写分离实现

操作 实现机制 性能影响
直接访问当前数组引用 无锁,极快
加锁→复制数组→修改→替换引用 复制开销大,阻塞其他写操作

3. 适用场景与限制

  • 推荐场景
    • 事件监听器列表(注册/注销少,触发多)
    • 配置黑白名单(更新频率低)
  • 规避问题
    • 大数据量时写操作导致Young GC频繁
    • 迭代器弱一致性(反映创建时的数组状态)

四、并发问题实战案例

1. ConcurrentHashMap复合操作陷阱

java 复制代码
// 错误用法:非原子性判断+修改
if (!map.containsKey(key)) {
    map.put(key, value); // 可能被其他线程打断
}

// 正确方案1:使用putIfAbsent
map.putIfAbsent(key, value);

// 正确方案2:compute原子方法
map.compute(key, (k, v) -> v == null ? value : v);

2. CopyOnWriteArrayList内存溢出案例

java 复制代码
List<byte[]> list = new CopyOnWriteArrayList<>();
while (true) {
    list.add(new byte[1024 * 1024]); // 每次复制1MB数组
    // 快速触发OutOfMemoryError
}

解决方案 :对于大数据集,考虑使用ConcurrentLinkedQueueBlockingQueue

五、并发容器性能测试

1. 百万次操作耗时对比(单位:ms)

操作 HashMap Hashtable ConcurrentHashMap Collections.synchronizedMap
100万次put 120 450 180 400
100万次get 80 220 100 210
100并发put+get混合 崩溃 3200 850 3000

2. 不同写比例下的QPS

java 复制代码
# 测试命令:JMH基准测试
@Benchmark
@Threads(8)
public void testCopyOnWrite(Blackhole bh) {
    list.get(0);  // 读操作
    if (writeRatio > 0 && rnd.nextDouble() < writeRatio) {
        list.add("newItem"); // 写操作
    }
}

结果

  • 读占比99%时:QPS 120,000
  • 读占比90%时:QPS 23,000
  • 读写各50%时:QPS 1,200

六、扩展容器选型

1. 其他高并发容器

容器 实现原理 推荐场景
ConcurrentSkipListMap 跳表(CAS) 需要有序的并发Map
ConcurrentLinkedQueue 无锁队列(CAS) 高吞吐的生产者-消费者模式
LinkedBlockingQueue 双锁队列(put/take分离) 有界阻塞队列(线程池任务队列)

2. 第三方库推荐

  • Caffeine:高性能本地缓存(替代Guava Cache)
  • Disruptor:无锁环形队列(金融级低延迟)
  • Agrona:零拷贝并发数据结构(航空级性能)

七、QA高频问题

💬 Q1:ConcurrentHashMap的size()为什么可能不精确?

答案

  • 采用分段计数(baseCount + CounterCell[]
  • 并发更新时优先尝试CAS修改baseCount,冲突时使用CounterCell分流
  • 最终结果是弱一致性的,但实际误差通常可忽略

💬 Q2:为什么COW迭代器不支持remove()?

设计原理

  • 迭代器持有的是旧数组的快照
  • 删除操作会影响新数组,导致数据不一致
  • 解决方案:使用CopyOnWriteArraySet或手动记录待删除元素

💬 Q3:JDK8的ConcurrentHashMap为什么不使用ReentrantLock?

性能考量

  • synchronized在JDK6后已大幅优化(偏向锁/轻量级锁)
  • 锁粒度细化到桶头节点后,竞争概率极低
  • 减少内存开销(每个Node无需携带Lock对象)

最佳实践建议

  1. 优先使用ConcurrentHashMap而非synchronizedMap
  2. CopyOnWriteArrayList适用于读多写少数据量小的场景
  3. 警惕复合操作的线程安全问题(使用原子方法如computeIfAbsent
  4. 监控容器内存占用(尤其COW类容器)

通过jmap -histo <pid>可查看容器内存占用情况

相关推荐
青松@FasterAI3 小时前
【大模型面试】大模型(LLMs)高频面题全面整理(★2025年5月最新版★)
面试·职场和发展·大模型开发·大模型面试·nlp面试·nlp面题·大模型面题
爱吃涮毛肚的肥肥(暂时吃不了版)4 小时前
仿腾讯会议——创建房间&加入房间
c++·qt·面试·职场和发展·腾讯会议
源码云商5 小时前
基于 SpringBoot + Vue 的校园管理系统设计与实现
vue.js·spring boot·后端
奔驰的小野码5 小时前
SpringAI实现AI应用-内置顾问
java·人工智能·后端·spring
普通人zzz~5 小时前
SpringBoot记录用户操作日志
java·spring boot·后端
大三开学菜鸟5 小时前
记录一下spring-cloud-starter-alibaba-nacos-config 2023.0.3.2与springboot版本及配置问题
java·spring boot·后端·spring·intellij-idea
_Power_Y6 小时前
面试算法刷题练习1(核心+acm)
算法·面试
gxh19926 小时前
springboot微服务连接nacos超时
linux·后端·微服务·nacos
bxp13217 小时前
springboot国家化多语言实现
java·spring boot·后端
程序员JerrySUN8 小时前
驱动开发硬核特训 · 专题篇:Vivante GPU 与 DRM 图形显示体系全解析(i.MX8MP 平台实战)
linux·驱动开发·嵌入式硬件·面试·职场和发展·架构