ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

04-List 到底该用哪个?ArrayList、LinkedList、Vector、CopyOnWriteArrayList 选型实战 单线程无脑 ArrayList读多写少并发用 COW上一讲理清楚了 ArrayList 和 LinkedList 的底层这一篇把它们和Vector、CopyOnWriteArrayList摆在一起给出一份能直接抄的选型决策表。不再背概念只看场景。一、四兄弟的身份卡类底层线程安全扩容随机读中间增删适用基调ArrayList动态数组否1.5 倍O(1)O(n)默认首选LinkedList双向链表否无O(n)O(1)*两端操作/DequeVector动态数组是方法 synchronized2 倍O(1)O(n)遗留别用CopyOnWriteArrayList数组副本是写时复制复制整份O(1)O(n) 且更贵读多写少* 已知节点引用时 O(1)否则要先定位 O(n)二、Vector被时代淘汰的线程安全Vector和ArrayList几乎同年出生区别是它每个方法都加了synchronizedpublicsynchronizedbooleanadd(Ee){...}// 方法级锁publicsynchronizedEget(intindex){...}问题在哪锁太粗读也要排队高并发下吞吐被锁死。复合操作不保证原子if (!vector.contains(x)) vector.add(x)这两步之间仍有窗口光靠方法锁没用。扩容是 2 倍比 ArrayList 的 1.5 倍更浪费。结论新代码别用 Vector。要线程安全的 List见下文 CopyOnWriteArrayList 或Collections.synchronizedList。三、Collections.synchronizedList折中的同步包装如果不想引入并发容器又要多线程安全可以用它包一层ListStringsafeCollections.synchronizedList(newArrayList());它的实现也很朴素——内部持有一把mutex每个方法synchronized(mutex)后委托给被包装的 list。缺点和 Vector 一样单方法安全但迭代、复合操作仍需你自己手动同步synchronized(safe){// 迭代必须自己加锁否则可能 CMEfor(Strings:safe){...}}适合写不多、且调用方愿意手动管锁的场景否则直接上并发容器。四、CopyOnWriteArrayList读多写少的王它的思路很绝写的时候复制一份新数组改完再原子替换引用读永远读旧数组、无锁。publicbooleanadd(Ee){finalReentrantLocklockthis.lock;lock.lock();try{Object[]elementsgetArray();intlenelements.length;Object[]newElementsArrays.copyOf(elements,len1);// 复制整份newElements[len]e;setArray(newElements);// 替换引用returntrue;}finally{lock.unlock();}}publicEget(intindex){returngetArray()[index];// 无锁直接读}特性鲜明读完全无锁、极高并发迭代器拿到的是快照遍历期间怎么改都不会ConcurrentModificationException。写极贵每次 add/set/remove 都复制整个数组O(n) 且吃内存。数据有短暂不一致写后读可能还看到旧值最终一致。所以它只适合读远多于写的场景比如系统配置项列表、监听器注册表、白名单——这些偶尔改、天天读的数据。如果用它做高频率写复制开销会拖垮性能。五、选型决策树直接抄需要线程安全吗 ├─ 否 → 需要频繁中间增删 迭代游标 → LinkedList │ 否则绝大多数情况 → ArrayList记得预设容量 │ └─ 是 → 读多写少配置/监听/白名单 → CopyOnWriteArrayList └─ 读写都频繁需要手动控锁 → Collections.synchronizedList └─ 千万别选 → Vector已淘汰一句话单线程默认 ArrayList读多写少并发用 CopyOnWriteArrayList需要栈/队列用 ArrayDeque 而非 LinkedList 的栈身份Vector 进博物馆。六、容易被忽略的几个点预设容量仍是第一优化选了 ArrayList批量 add 前ensureCapacity性能立竿见影见扩容篇。subList 是视图ArrayList 和 LinkedList 的 subList 都是原列表视图改一方影响另一方要独立就new ArrayList(subList)。equals 决定 contains/remove 行为List 的contains、indexOf、remove(Object)全靠元素的equals。元素没重写 equals就去比引用可能明明一样却查不到。Arrays.asList 返回的是定长 ListListString l Arrays.asList(a,b)底层是数组不能 add/remove抛UnsupportedOperationException。想要可变列表要new ArrayList(Arrays.asList(...))。七、一个真实踩坑某服务用Vector存在线用户读 QPS 上万写很少。表面线程安全很省心结果压测时吞吐上不去——所有读都卡在synchronized上排队。换成CopyOnWriteArrayList后读完全无锁吞吐翻了好几倍。教训“线程安全不等于高性能”得看读写比例选对结构。总结四选一不靠背诵靠场景单线程无脑 ArrayList预设容量读多写少并发选 CopyOnWriteArrayList需要手动控锁用 synchronizedListVector 已淘汰别碰栈/队列请用 ArrayDeque。锁的粒度、读写比例、内存开销才是选型的三个真正旋钮。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进