Java集合线程安全实践:从ArrayList数据迁移问题到synchronizedList解决方案

一、先说结论吧

在多线程环境下选择合适的List实现至关重要。下表总结了常见List实现的特性:

集合类型 线程安全 读性能 写性能 内存占用 使用场景与注意事项
ArrayList ❌ 不安全 ⭐⭐⭐⭐⭐ 直接索引访问 ⭐⭐⭐⭐ 尾部插入高效 ⭐⭐⭐⭐ 连续存储 单线程场景 随机访问频繁,尾部插入多
LinkedList ❌ 不安全 ⭐⭐ 需遍历节点 ⭐⭐⭐⭐⭐ 任意位置插入高效 ⭐⭐ 节点开销大 频繁插入删除 头尾操作多的队列场景
Vector ✅ 安全 ⭐ 全局锁阻塞 ⭐ 全局锁阻塞 ⭐⭐⭐⭐ 连续存储 已过时 遗留代码兼容,不推荐新项目使用
synchronizedList ✅ 安全 ⭐⭐ 方法级同步 ⭐⭐ 方法级同步 ⭐⭐⭐⭐ 包装器模式 通用线程安全方案 注意:复合操作需手动同步
CopyOnWriteArrayList ✅ 安全 ⭐⭐⭐⭐⭐ 无锁读快照 ⭐ 写时复制开销大 ⭐ 多版本存储 读多写极少 迭代器安全,写操作昂贵

二、实战案例:数据迁移中的线程安全问题

最近在数据迁移任务中,我遇到了一个典型的并发问题。先看问题代码:

复制代码
public void migrateTemplate() throws InterruptedException {
    // 问题点:使用非线程安全的ArrayList
    List<CustomerRotationLog> targets = new ArrayList<>();
    
    // 从MongoDB获取待迁移数据
    List<MongodbDynamicMessage> all = mongodbDynamicMessageMapper.findAll();
    
    // 创建线程计数器,用于同步所有转换任务
    CountDownLatch latch = new CountDownLatch(all.size());

    for (MongodbDynamicMessage source : all) {
        // 使用线程池异步转换
        ThreadPoolUtil.executeSafely(() -> {
            try {
                // 数据转换
                CustomerRotationLog target = convert(source);
                targets.add(target); // 🚨 线程不安全操作!
            } finally {
                latch.countDown();
                System.out.println("处理进度:" + latch.getCount() + "/" + all.size());
            }
        });
    }
    
    // 等待所有任务完成
    latch.await();
    // 批量保存
    commonService.processInBatch(targets, baseMapper::batchInsert);
}

问题分析

这段代码表面逻辑清晰,但实际上存在严重的线程安全问题 。最终的targets.size()很可能会小于all.size()

根本原因ArrayListadd()方法是非线程安全的。在多线程并发调用时,两个主要操作:

  1. elementData[size] = element; // 存储元素

  2. size = size + 1; // 更新大小

这两个操作不是原子的,可能导致:

  • 数据覆盖:多个线程对同一位置赋值

  • 大小不一致size更新不及时或错误

  • ArrayIndexOutOfBoundsException:扩容过程中的竞争条件

三、解决方案:使用synchronizedList

针对上述问题,最简单的修复方案是使用Collections.synchronizedList()

复制代码
public void migrateTemplate() throws InterruptedException {
    // 解决方案:使用线程安全的synchronizedList
    List<CustomerRotationLog> targets = Collections.synchronizedList(new ArrayList<>());
    
    List<MongodbDynamicMessage> all = mongodbDynamicMessageMapper.findAll();
    CountDownLatch latch = new CountDownLatch(all.size());

    for (MongodbDynamicMessage source : all) {
        ThreadPoolUtil.executeSafely(() -> {
            try {
                CustomerRotationLog target = convert(source);
                targets.add(target); // ✅ 现在是线程安全的
            } finally {
                latch.countDown();
                System.out.println("处理进度:" + latch.getCount() + "/" + all.size());
            }
        });
    }
    
    latch.await();
    commonService.processInBatch(targets, baseMapper::batchInsert);
}

四、总结与最佳实践

  1. 线程安全是第一要务:在多线程环境下操作共享集合,必须选择线程安全的实现

  2. 根据场景选择方案

    • 读多写极少 :选CopyOnWriteArrayList,迭代安全,无需额外同步

    • 写多或读写均衡 :选synchronizedList,注意复合操作的手动同步

  3. 理解实现原理

    • synchronizedList:方法级同步锁,适合通用场景

    • CopyOnWriteArrayList:写时复制,适合读主导场景

  4. 避免使用遗留类Vector已过时,其设计不适合现代并发需求

相关推荐
m0_613856291 小时前
mysql如何利用事务隔离级别解决特定业务冲突_mysql隔离方案选型
jvm·数据库·python
abcnull1 小时前
用javaparser做精准测试
java·ast·静态代码分析·精准测试·javaparser
叶小鸡1 小时前
Java 篇-项目实战-苍穹外卖-笔记汇总
java·开发语言·笔记
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第22题:HashMap 和 HashSet 有哪些区别
java·开发语言·哈希算法·散列表·hash
昵称小白2 小时前
复杂度分析方法
算法
juniperhan2 小时前
Flink 系列第21篇:Flink SQL 函数与 UDF 全解读:类型推导、开发要点与 Module 扩展
java·大数据·数据仓库·分布式·sql·flink
科研前沿2 小时前
2026 数字孪生前沿科技:全景迭代报告 —— 镜像视界生成式孪生(Generative DT)技术白皮书
大数据·人工智能·科技·算法·音视频·空间计算
ID_180079054732 小时前
Python 实现亚马逊商品详情 API 数据准确性校验(极简可用 + JSON 参考)
java·python·json
c++之路2 小时前
C++23概述
java·c++·c++23
专注API从业者3 小时前
Open Claw 京东商品监控选品实战:一键抓取、实时监控、高效选品
java·服务器·数据库