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

相关推荐
D龙源1 分钟前
VSCode-IoC和DI
后端·架构
陵易居士17 分钟前
Spring如何解决项目中的循环依赖问题?
java·后端·spring
独立开阀者_FwtCoder19 分钟前
# 白嫖千刀亲测可行——200刀拿下 Cursor、V0、Bolt和Perplexity 等等 1 年会员
前端·javascript·面试
Aska_Lv30 分钟前
RocketMQ---core原理
后端
AronTing35 分钟前
10-Spring Cloud Alibaba 之 Dubbo 深度剖析与实战
后端·面试·架构
没逻辑39 分钟前
⏰ Redis 在支付系统中作为延迟任务队列的实践
redis·后端
雷渊41 分钟前
如何保证数据库和Es的数据一致性?
java·后端·面试
fjkxyl42 分钟前
Spring的启动流程
java·后端·spring
掘金酱43 分钟前
😊 酱酱宝的推荐:做任务赢积分“拿”华为MatePad Air、雷蛇机械键盘、 热门APP会员卡...
前端·后端·trae
总之就是非常可爱1 小时前
🚀 使用 ReadableStream 优雅地处理 SSE(Server-Sent Events)
前端·javascript·后端