04-List 到底该用哪个?ArrayList、LinkedList、Vector、CopyOnWriteArrayList 选型实战

单线程无脑 ArrayList,读多写少并发用 COW

上一讲理清楚了 ArrayList 和 LinkedList 的底层,这一篇把它们和 Vector、CopyOnWriteArrayList 摆在一起,给出一份能直接抄的"选型决策表"。不再背概念,只看场景。

一、四兄弟的身份卡

类 底层 线程安全 扩容 随机读 中间增删 适用基调
ArrayList 动态数组 否 1.5 倍 O(1) O(n) 默认首选
LinkedList 双向链表 否 无 O(n) O(1)* 两端操作/Deque
Vector 动态数组 是(方法 synchronized) 2 倍 O(1) O(n) 遗留,别用
CopyOnWriteArrayList 数组副本 是(写时复制) 复制整份 O(1) O(n) 且更贵 读多写少

* 已知节点引用时 O(1),否则要先定位 O(n)

二、Vector:被时代淘汰的"线程安全"

Vector 和 ArrayList 几乎同年出生,区别是它每个方法都加了 synchronized:

java 复制代码
public synchronized boolean add(E e) { ... }   // 方法级锁
public synchronized E get(int index) { ... }

问题在哪?

  • 锁太粗:读也要排队,高并发下吞吐被锁死。
  • 复合操作不保证原子 :if (!vector.contains(x)) vector.add(x) 这两步之间仍有窗口,光靠方法锁没用。
  • 扩容是 2 倍,比 ArrayList 的 1.5 倍更浪费。

结论:新代码别用 Vector 。要线程安全的 List,见下文 CopyOnWriteArrayList 或 Collections.synchronizedList。

三、Collections.synchronizedList:折中的同步包装

如果不想引入并发容器,又要多线程安全,可以用它包一层:

java 复制代码
List<String> safe = Collections.synchronizedList(new ArrayList<>());

它的实现也很朴素------内部持有一把 mutex,每个方法 synchronized(mutex) 后委托给被包装的 list。缺点和 Vector 一样:单方法安全,但迭代、复合操作仍需你自己手动同步:

java 复制代码
synchronized (safe) { // 迭代必须自己加锁,否则可能 CME
    for (String s : safe) { ... }
}

适合"写不多、且调用方愿意手动管锁"的场景,否则直接上并发容器。

四、CopyOnWriteArrayList:读多写少的王

它的思路很绝:写的时候复制一份新数组,改完再原子替换引用,读永远读旧数组、无锁。

java 复制代码
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制整份
        newElements[len] = e;
        setArray(newElements); // 替换引用
        return true;
    } finally { lock.unlock(); }
}
public E get(int index) {
    return getArray()[index]; // 无锁,直接读
}

特性鲜明:

  • 读完全无锁、极高并发 ,迭代器拿到的是"快照",遍历期间怎么改都不会 ConcurrentModificationException。
  • 写极贵:每次 add/set/remove 都复制整个数组,O(n) 且吃内存。
  • 数据有短暂不一致:写后读可能还看到旧值(最终一致)。

所以它只适合读远多于写的场景:比如系统配置项列表、监听器注册表、白名单------这些"偶尔改、天天读"的数据。如果用它做高频率写,复制开销会拖垮性能。

五、选型决策树(直接抄)

复制代码
需要线程安全吗?
├─ 否 → 需要频繁中间增删 + 迭代游标?  → LinkedList
│      否则(绝大多数情况)            → ArrayList(记得预设容量)
│
└─ 是 → 读多写少(配置/监听/白名单)?  → CopyOnWriteArrayList
        └─ 读写都频繁,需要手动控锁?    → Collections.synchronizedList
        └─ 千万别选                      → Vector(已淘汰)

一句话:单线程默认 ArrayList;读多写少并发用 CopyOnWriteArrayList;需要栈/队列用 ArrayDeque 而非 LinkedList 的栈身份;Vector 进博物馆。

六、容易被忽略的几个点

  1. 预设容量仍是第一优化 :选了 ArrayList,批量 add 前 ensureCapacity,性能立竿见影(见扩容篇)。
  2. subList 是视图 :ArrayList 和 LinkedList 的 subList 都是原列表视图,改一方影响另一方,要独立就 new ArrayList<>(subList)。
  3. equals 决定 contains/remove 行为 :List 的 contains、indexOf、remove(Object) 全靠元素的 equals。元素没重写 equals,就去比引用,可能"明明一样却查不到"。
  4. Arrays.asList 返回的是定长 List :List<String> l = Arrays.asList("a","b") 底层是数组,不能 add/remove(抛 UnsupportedOperationException)。想要可变列表要 new ArrayList<>(Arrays.asList(...))。

七、一个真实踩坑

某服务用 Vector 存在线用户,读 QPS 上万,写很少。表面"线程安全很省心",结果压测时吞吐上不去------所有读都卡在 synchronized 上排队。换成 CopyOnWriteArrayList 后,读完全无锁,吞吐翻了好几倍。教训:"线程安全"不等于"高性能",得看读写比例选对结构。

总结

四选一不靠背诵靠场景:单线程无脑 ArrayList(预设容量);读多写少并发选 CopyOnWriteArrayList;需要手动控锁用 synchronizedList;Vector 已淘汰别碰;栈/队列请用 ArrayDeque。锁的粒度、读写比例、内存开销,才是选型的三个真正旋钮。

相关推荐
Wang's Blog1 小时前
Java 中间件之 RabbitMQ 快速入门: SpringAMQP WorkQueue 工作队列模型
java·中间件·java-rabbitmq
代码山河1 小时前
第一个Java程序:Hello World从零编写与运行
java·学习·架构·教程·面向对象·项目
codigger1 小时前
Java 26 深度解析:一次“性能兑现 + 契约收紧”的版本
java·后端·编程·编程语言
盖世英雄酱581361 小时前
格式化成日期为什么提前一天呢,同样的时间点还是非必现???
java·后端
威哥爱编程1 小时前
【AI全栈12-05】Spring Boot 用 Tool Calling 让 AI 查实时订单与库存
java·spring boot·后端
Guarding and trust1 小时前
项目拆解(二):模型输出是‘野‘的——三层兜底+重分析,把AI变成可靠零件
java·人工智能·python·大模型·fastapi·ollama·错误兜底
春风霓裳2 小时前
0xc0000005 = 内存访问违规
java
威哥爱编程2 小时前
【AI全栈12-06】Spring Boot 用 RAG 搭企业私有知识库问答
java·spring boot·后端
迅猛龙办公室2 小时前
Python实现获取星期字符串
java·前端·python