多线程共享CopyOnWriteArrayList:添加元素后遍历与size不符疑问
为什么CopyOnWriteArrayList添加元素后,主线程遍历还是看不到新元素?
先看你提供的代码:
public class ThreadExample extends Thread { static List<Integer> myList = new CopyOnWriteArrayList<Integer>(); public static void main(String[] args) throws InterruptedException { myList.add(11); myList.add(22); myList.add(33); myList.add(44); ThreadExample e = new ThreadExample(); e.start(); for (Integer it : myList) { try { Thread.sleep(1000); } catch (Exception e1) { System.out.print("e1 "); } System.out.print(" " + it); } } public void run() { try { Thread.sleep(50); } catch (Exception e) { System.out.print("e2 "); } myList.add(77); System.out.print("size: " + myList.size() + ", elements:"); } }
你的困惑完全合理:工作线程明明先完成了add(77)操作,列表的size()也正确返回了5,但主线程遍历却只输出4个元素。这背后的核心原因是CopyOnWriteArrayList的「写时复制+迭代器快照」特性。
具体拆解事件顺序:
- 主线程先往列表里加入4个元素,启动工作线程后,立刻进入
for-each循环。 - 当
for-each循环启动时,会隐式创建一个CopyOnWriteArrayList的迭代器。这个迭代器是基于创建它那一刻的列表底层数组快照构建的——此时工作线程还在50ms的sleep中,列表只有4个元素,所以迭代器拿到的就是这4个元素的快照。 - 工作线程sleep结束后执行
myList.add(77):CopyOnWriteArrayList的写操作会复制一份当前的底层数组,在新数组上添加元素77,再把列表的底层引用切换到新数组。这时候调用size()会返回新数组的长度5,所以你能看到size: 5的输出。 - 但主线程的迭代器已经绑定了旧的、只有4个元素的数组快照,它不会感知到后续底层数组的替换。所以哪怕列表现在实际有5个元素,迭代器还是会老老实实遍历旧快照里的4个元素。
验证这个逻辑
如果我们修改主线程,让它在进入循环前等待工作线程完成添加操作,比如加一行Thread.sleep(100):
e.start(); Thread.sleep(100); // 等待工作线程完成元素添加 for (Integer it : myList) { // ... 原代码 }
这时候迭代器会在工作线程添加元素后创建,拿到的是包含5个元素的快照,遍历就会输出11 22 33 44 77了。
简单来说,CopyOnWriteArrayList的迭代器是「只读快照」,它和后续的写操作完全隔离——这也是它在并发场景下无需加锁就能安全遍历的原因,但代价就是迭代器无法实时反映列表的最新状态。
内容的提问来源于stack exchange,提问作者Abhilash
相关产品推荐
相关产品推荐

