咸阳市网站建设_网站建设公司_Tailwind CSS_seo优化
2025/12/31 19:36:07 网站建设 项目流程

📖目录

  • 前言:你写的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 源码深度拆解:如何做到高性能并发?》

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询