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>可查看容器内存占用情况

相关推荐
yuuki23323321 分钟前
【C语言】文件操作(附源码与图片)
c语言·后端
IT_陈寒25 分钟前
Python+AI实战:用LangChain构建智能问答系统的5个核心技巧
前端·人工智能·后端
DIY机器人工房36 分钟前
【嵌入式面试题】STM32F103C8T6 完整元器件解析 + 面试问题答案
stm32·单片机·面试·嵌入式·面试题·diy机器人工房
无名之辈J1 小时前
系统崩溃(OOM)
后端
晴殇i1 小时前
前端鉴权新时代:告别 localStorage,拥抱更安全的 JWT 存储方案
前端·javascript·面试
来旺1 小时前
互联网大厂Java面试全解析及三轮问答专项
java·数据库·spring boot·安全·缓存·微服务·面试
码农刚子1 小时前
ASP.NET Core Blazor简介和快速入门 二(组件基础)
javascript·后端
间彧1 小时前
Java ConcurrentHashMap如何合理指定初始容量
后端
catchadmin1 小时前
PHP8.5 的新 URI 扩展
开发语言·后端·php
小奋斗1 小时前
面试官:[1] == '1'和[1] == 1结果是什么?
前端·面试