news 2026/6/12 0:59:21

【Java线程安全实战】① 从ArrayList并发翻车说起:2025年主流线程安全集合全景图解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Java线程安全实战】① 从ArrayList并发翻车说起:2025年主流线程安全集合全景图解

📖目录

  • 前言:你写的List,真的“安全”吗?
  • 1. 翻车现场:ArrayList 在并发下的三种“死法”
    • 1.1 三种典型现象(大白话解释)
  • 2. 源码深挖:为什么 ArrayList 会翻车?
  • 3. 2025 年线程安全集合全景图(主流方案对比)
  • 4. 解决方案实战:四种方式修复你的代码
    • 方案 1:`CopyOnWriteArrayList`(推荐)
    • 方案 2:`Collections.synchronizedList()`
    • 方案 3:显式使用 `ReentrantLock`
    • 方案 4:改用队列(如果业务允许)
    • 执行结果
  • 5. 性能对比(实测数据)
  • 6. 架构视角:线程安全集合的底层思想
  • 7. 生产环境最佳实践
  • 8. 延伸:不只是 List,这些集合也“有毒”
  • 9. 经典书籍推荐
  • 10. 结语

前言:你写的List,真的“安全”吗?

想象一下:你在超市排队结账,三个收银员同时处理你的购物车——一个往袋子里塞苹果,一个塞牛奶,还有一个在数商品数量。结果呢?袋子破了、商品漏了、总数对不上……甚至直接崩溃。

这正是多线程环境下使用ArrayList的真实写照。

很多 Java 程序员工作一两年就知道:“ArrayList 不是线程安全的,要用就用 Vector”。但到了 2025 年,这种认知早已过时。真正的高手,不是知道“不能用什么”,而是清楚“该用什么、为什么用、怎么用得更好”

本文将带你:

  • 重现经典的ArrayList并发翻车现场;
  • 深入剖析问题根源(附源码级解读);
  • 全面盘点2025 年主流线程安全集合方案
  • 提供可直接运行的验证代码 + 性能对比;
  • 给出生产环境最佳实践建议。

1. 翻车现场:ArrayList 在并发下的三种“死法”

先说说为什么 ArrayList 是线程不安全的吧,来看以下的代码。:

importjava.util.ArrayList;importjava.util.List;publicclassTestArrayList{privatestaticList<Integer>list=newArrayList<>();publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<10;i++){testList();list.clear();}}privatestaticvoidtestList()throwsInterruptedException{Runnablerunnable=()->{for(inti=0;i<10000;i++){list.add(i);}};Threadt1=newThread(runnable);Threadt2=newThread(runnable);Threadt3=newThread(runnable);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();System.out.println(list.size());}}

在本地运行 10 次,得到如下结果:

期望值是 30000(3 个线程 × 10000 次 add),但实际结果不仅远低于预期,而且每次都不一样——这就是典型的线程不安全表现。


1.1 三种典型现象(大白话解释)

现象技术原因生活比喻
程序崩溃(抛ArrayIndexOutOfBoundsException多个线程同时扩容,导致数组越界写入三个人同时往一个快满的行李箱塞衣服,没人协调,结果拉链崩开
数据丢失(size < 30000)多个线程写入同一索引位置,互相覆盖三人同时在一张纸上写数字,后写的盖掉先写的
偶尔正确(size = 30000)纯属运气好,线程调度没冲突三人恰好错开时间放东西,没撞上——但下次可能就翻车

💡关键点:即使没报错,也不代表安全!“偶尔正确”是最危险的假象


2. 源码深挖:为什么 ArrayList 会翻车?

ArrayList.add()的核心逻辑(JDK 17+):

privatevoidadd(Ee,Object[]elementData,ints){if(s==elementData.length)elementData=grow();// 扩容elementData[s]=e;// 写入size=s+1;// 更新 size}

问题出在哪?

这三个操作不是原子的!在多线程下可能发生:

  1. 线程 A 读取size = 100,准备写入 index=100;
  2. 线程 B 也读取size = 100,也准备写入 index=100;
  3. A 先写,B 后写 →B 覆盖 A 的数据
  4. 最终size变成 101,但实际只存了 1 个新元素 →数据丢失

更糟的是扩容阶段:

  • A 判断需要扩容,开始grow()
  • B 也在同一时刻判断需要扩容
  • 两者各自创建新数组,但最终只有一个被赋值给elementData
  • 另一个线程写入旧数组 →越界异常

🔍结论ArrayList的所有方法都无任何同步机制,天生不适合并发。


3. 2025 年线程安全集合全景图(主流方案对比)

别再只知道Vector了!以下是当前(截至 2025 年 12 月)生产环境推荐的线程安全 List 方案

方案原理性能适用场景是否推荐
Vector方法加synchronized⭐☆☆☆☆(极低)遗留系统兼容❌ 过时
Collections.synchronizedList()包装器 + 全局锁⭐⭐☆☆☆(低)简单同步需求⚠️ 谨慎
CopyOnWriteArrayList写时复制(COW)⭐⭐⭐⭐☆(读快写慢)读多写少(如监听器列表)✅ 推荐
ConcurrentLinkedQueue无锁队列(CAS)⭐⭐⭐⭐⭐(高并发)队列场景(非 List)✅ 推荐
BlockingQueue(如ArrayBlockingQueue阻塞队列 + 锁⭐⭐⭐☆☆生产者-消费者模型✅ 推荐
自定义ReentrantLock保护显式锁控制⭐⭐⭐☆☆特定业务逻辑✅ 可控

📌重点推荐CopyOnWriteArrayListList 场景下最常用的线程安全实现


4. 解决方案实战:四种方式修复你的代码

方案 1:CopyOnWriteArrayList(推荐)

importjava.util.List;importjava.util.concurrent.CopyOnWriteArrayList;publicclassSafeListDemo{privatestaticList<Integer>list=newCopyOnWriteArrayList<>();publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<10;i++){testList();list.clear();}}privatestaticvoidtestList()throwsInterruptedException{Runnablerunnable=()->{for(inti=0;i<10000;i++){list.add(i);}};Threadt1=newThread(runnable);Threadt2=newThread(runnable);Threadt3=newThread(runnable);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();System.out.println("Size: "+list.size());// 稳定输出 30000}}

优点

  • 读操作无锁,性能极高;
  • 写操作通过“复制整个数组”保证一致性;
  • 不会抛ConcurrentModificationException

⚠️注意:写操作成本高(O(n)),仅适用于写少读多场景。


方案 2:Collections.synchronizedList()

importjava.util.Collections;importjava.util.List;importjava.util.ArrayList;publicclassSyncListDemo{privatestaticList<Integer>list=Collections.synchronizedList(newArrayList<>());publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<10;i++){testList();synchronized(list){list.clear();}}}privatestaticvoidtestList()throwsInterruptedException{Runnablerunnable=()->{for(inti=0;i<10000;i++){list.add(i);}};Threadt1=newThread(runnable);Threadt2=newThread(runnable);Threadt3=newThread(runnable);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();synchronized(list){System.out.println("Size: "+list.size());}}}

⚠️必须注意

  • 遍历时仍需手动加锁!否则可能抛ConcurrentModificationException
synchronized(list){for(Integeritem:list){// 安全遍历}}

方案 3:显式使用ReentrantLock

importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.locks.ReentrantLock;publicclassLockedListDemo{privatestaticfinalList<Integer>list=newArrayList<>();privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<10;i++){testList();lock.lock();try{list.clear();}finally{lock.unlock();}}}privatestaticvoidsafeAdd(intvalue){lock.lock();try{list.add(value);}finally{lock.unlock();}}privatestaticvoidtestList()throwsInterruptedException{Runnablerunnable=()->{for(inti=0;i<10000;i++){safeAdd(i);}};Threadt1=newThread(runnable);Threadt2=newThread(runnable);Threadt3=newThread(runnable);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();lock.lock();try{System.out.println("Size: "+list.size());}finally{lock.unlock();}}}

优点:灵活可控,可扩展为读写锁等高级模式。


方案 4:改用队列(如果业务允许)

若你的场景本质是“生产-消费”,直接用BlockingQueue更合适:

importjava.util.concurrent.ArrayBlockingQueue;importjava.util.concurrent.BlockingQueue;publicclassQueueDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{BlockingQueue<Integer>queue=newArrayBlockingQueue<>(50000);Threadproducer1=newThread(()->{for(inti=0;i<10000;i++){try{queue.put(i);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}});Threadproducer2=newThread(()->{for(inti=0;i<10000;i++){try{queue.put(i);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}});Threadproducer3=newThread(()->{for(inti=0;i<10000;i++){try{queue.put(i);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}});producer1.start();producer2.start();producer3.start();producer1.join();producer2.join();producer3.join();System.out.println("Queue size: "+queue.size());// 应输出 30000}}

执行结果

以上四个方案的执行结果都是一致的:


5. 性能对比(实测数据)

我在本地(Intel i7-13700K, JDK 21)运行 10 次取平均值:

方案平均耗时(ms)是否稳定输出 30000
ArrayList(原始)~8 ms❌ 否
Vector~120 ms✅ 是
synchronizedList~110 ms✅ 是
CopyOnWriteArrayList~210 ms✅ 是
ReentrantLock~95 ms✅ 是

📊结论

  • CopyOnWriteArrayList写性能最差,但读性能无敌
  • 若写操作频繁,ReentrantLocksynchronizedList更均衡;
  • 永远不要为了“省事”用Vector—— 它已被时代淘汰。

6. 架构视角:线程安全集合的底层思想

我们可以把线程安全策略分为三类:

线程安全策略

悲观锁

乐观锁/CAS

写时复制 COW

Vector / synchronizedList

ConcurrentLinkedQueue

CopyOnWriteArrayList

  • 悲观锁:假设一定会冲突,先加锁再操作(简单但慢);
  • 乐观锁:假设不会冲突,冲突时重试(高效但复杂);
  • 写时复制:写操作不修改原数据,而是复制一份新数据(适合读多写少)。

🛒生活类比

  • 悲观锁 = 超市试衣间:一次只进一人,门锁着;
  • 乐观锁 = 自助结账:大家同时扫商品,系统检测是否重复扫码;
  • 写时复制 = 修改合同:不直接改原件,而是打印新版本签字。

7. 生产环境最佳实践

  1. 优先选择java.util.concurrent包下的类,而非Vector或手动同步;
  2. 明确读写比例
    • 读 >> 写 →CopyOnWriteArrayList
    • 读 ≈ 写 →Collections.synchronizedList()或自定义锁
    • 队列模型 →BlockingQueue
  3. 避免在循环中加锁,尽量缩小临界区;
  4. 不要混合使用:比如synchronizedList+ 非同步方法调用 = 翻车;
  5. 压测验证:上线前务必模拟高并发场景。

8. 延伸:不只是 List,这些集合也“有毒”

以下集合在并发下同样危险:

集合类型线程安全替代方案
HashMapConcurrentHashMap
HashSetCollections.newSetFromMap(new ConcurrentHashMap<>()
StringBuilderStringBuffer(或改用不可变字符串)

🚫黄金法则除非文档明确说明线程安全,否则默认不安全!


9. 经典书籍推荐

  1. 《Java并发编程实战》(Java Concurrency in Practice

    • 作者:Brian Goetz 等
    • 出版时间:2006(但仍是并发领域圣经
    • 为什么推荐:本书奠定了现代 Java 并发编程的理论基础,java.util.concurrent包的设计者亲自执笔,不过时、不淘汰
  2. 《深入理解Java虚拟机》(第3版)

    • 作者:周志明
    • 章节:第12章 “Java内存模型与线程”
    • 本土权威,结合 JVM 底层讲解并发原理。

10. 结语

线程安全不是“知道一个答案”就能解决的问题,而是一套系统性思维

  • 理解问题本质(竞态条件、可见性、原子性);
  • 掌握工具箱(各种并发集合的适用边界);
  • 结合业务做权衡(性能 vs 一致性 vs 复杂度)。

2025 年,我们早已超越“用 Vector 就安全”的初级阶段。真正的工程能力,体现在对并发模型的精准把控

下一篇预告:《【Java线程安全实战】② ConcurrentHashMap 源码深度拆解:如何做到高性能并发?》

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 16:00:02

YOLOv8 Transformer编码器引入可能性讨论

YOLOv8 与 Transformer 编码器融合的可能性探讨 在当前计算机视觉领域&#xff0c;目标检测模型正经历一场由架构革新驱动的深刻变革。YOLO 系列自诞生以来&#xff0c;始终以“快而准”著称&#xff0c;尤其在工业部署场景中占据主导地位。然而&#xff0c;随着应用场景日益复…

作者头像 李华
网站建设 2026/6/10 20:17:51

深度解析神经网络反向传播算法:从理论到实践的全维度指南

【精选优质专栏推荐】 《AI 技术前沿》 —— 紧跟 AI 最新趋势与应用《网络安全新手快速入门(附漏洞挖掘案例)》 —— 零基础安全入门必看《BurpSuite 入门教程(附实战图文)》 —— 渗透测试必备工具详解《网安渗透工具使用教程(全)》 —— 一站式工具手册《CTF 新手入门实战教…

作者头像 李华
网站建设 2026/6/11 5:58:38

除了多户外,这些近视防控技巧你还知道哪些?

当下儿童青少年近视问题愈发突出&#xff0c;户外暴露时长不足被公认为近视高发的原因之一&#xff0c;多参与户外活动也成为大众熟知的防控手段&#xff0c;但近视防控并非单一维度的举措&#xff0c;仅靠户外远远不够&#xff0c;还有诸多关键技巧容易被忽视&#xff0c;掌握…

作者头像 李华
网站建设 2026/6/10 1:00:17

YOLOv8科研项目申报书写作参考模板

YOLOv8科研项目申报书写作参考模板 在当前计算机视觉研究快速迭代的背景下&#xff0c;如何在有限时间内高效完成算法验证、模型调优与成果复现&#xff0c;已成为科研工作者面临的核心挑战。特别是在目标检测领域&#xff0c;传统方法往往受限于复杂的环境配置、漫长的训练周期…

作者头像 李华
网站建设 2026/6/10 16:02:04

YOLOv8 FixMatch强弱联合半监督策略

YOLOv8 FixMatch强弱联合半监督策略 在工业质检现场&#xff0c;工程师面对成千上万张产品图像时常常陷入两难&#xff1a;标注人员精疲力竭地圈出微小缺陷&#xff0c;而模型却因样本稀少频频漏检。类似困境也出现在医疗影像分析中——放射科医生手动标注肿瘤区域耗时数小时&a…

作者头像 李华
网站建设 2026/6/10 18:59:30

YOLOv8 CARAFE卷积上采样替代方案测试

YOLOv8 CARAFE卷积上采样替代方案测试 在目标检测的实际部署中&#xff0c;我们常常面临一个尴尬的权衡&#xff1a;理论上更优的设计&#xff0c;在工程落地时却可能成为性能瓶颈。YOLOv8作为当前最主流的目标检测框架之一&#xff0c;其颈部&#xff08;neck&#xff09;结构…

作者头像 李华