Collections.synchronizedList()常被当作"给List加把锁就万事大吉"的万能方案,这篇真实验证它到底保护了什么、没保护什么——遍历这件事,真实还是得自己动手加锁。
10个线程并发调用add(),对比synchronizedList和普通ArrayList的真实结果。
CollectionsSynchronizedWrapperDemo.java List<Integer> syncList = Collections.synchronizedList(new ArrayList<>()); // 10个线程各自add()10000次
真实10线程并发add(),最终size: 100000(期望: 100000) 普通ArrayList并发add(),最终size: 22628(期望: 100000,真实是否吻合: false,是否真实触发过异常: false)
既然synchronizedList号称"线程安全",遍历它是不是也不用操心了?真实测20轮看结果。
CollectionsSynchronizedWrapperDemo.java for (Integer x : syncList2) { /* 没有手动synchronized包裹这个遍历 */ } // 同时另一个线程在真实调用syncList2.add()
第一次: 20轮真实测试中触发ConcurrentModificationException的轮数: 3 第二次: 20轮真实测试中触发ConcurrentModificationException的轮数: 1
真实结果证明了 synchronizedList 提供的保护范围有一条明确的边界:它真实只保护单个方法调用内部的操作原子性,不保护"遍历"这种由多次方法调用(hasNext() + next() 反复调用)组成的复合操作。迭代器本身依然真实继承了 ArrayList 的 fail-fast 机制(和系列前面 fail-fast 迭代器那篇验证过的 modCount 检测完全一样),如果遍历期间另一个线程真实调用了 add()(即使这次 add() 调用本身是线程安全的),迭代器依然会真实检测到"结构被修改"并抛出异常。
CollectionsSynchronizedWrapperDemo.java synchronized (syncList3) { // 手动用这个list对象本身作为锁 for (Integer x : syncList3) { ... } }
这是 JDK 官方文档明确要求的正确写法:synchronizedList() 内部所有方法真实都是用返回的这个 List 对象本身作为锁,所以只要在遍历外面手动加一个 synchronized(list) 块,就能真实和内部方法用的是同一把锁,阻止其他线程在遍历期间执行任何修改操作——这也是这套 Collections.synchronizedXxx() 系列包装类被普遍认为"用起来别扭、容易被误用"的根本原因,后来的 java.util.concurrent 包(如 CopyOnWriteArrayList、ConcurrentHashMap)在设计上都真实针对性地解决了这个"遍历安全性"的痛点。
每一行都是真实反射或真实运行这段代码得到的结果。