单线程无脑 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 进博物馆。
六、容易被忽略的几个点
- 预设容量仍是第一优化 :选了 ArrayList,批量 add 前
ensureCapacity,性能立竿见影(见扩容篇)。 - subList 是视图 :ArrayList 和 LinkedList 的 subList 都是原列表视图,改一方影响另一方,要独立就
new ArrayList<>(subList)。 - equals 决定 contains/remove 行为 :List 的
contains、indexOf、remove(Object)全靠元素的equals。元素没重写 equals,就去比引用,可能"明明一样却查不到"。 - 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。锁的粒度、读写比例、内存开销,才是选型的三个真正旋钮。