文章目录
- 从一个尴尬的场景说起
- 什么是CopyOnWriteArrayList?
- 核心原理:写时复制机制
- 读写分离的艺术
- 关键技术实现
- 深入源码:看看实际如何工作
- 添加元素的过程
- 读取元素的极致简单
- 优缺点分析:没有银弹,只有合适场景
- 优势明显
- 缺点不容忽视
- 实战应用场景
- 1. 事件监听器列表
- 2. 缓存系统
- 3. JDBC驱动注册
- 与Vector的对比:为什么选择CopyOnWriteArrayList?
- 使用技巧与注意事项
- 总结
- 参考文章
大家好,我是你们的技术老友科威舟,今天给大家分享一下JUC里面的CopyOnWriteArrayList。
多线程并发修改数据,让读取操作不再阻塞,解锁高性能并发编程秘诀
Java并发包中一个非常有趣的线程安全容器——CopyOnWriteArrayList。它凭借独特的设计思想,巧妙解决了读多写少场景下的并发性能问题。
从一个尴尬的场景说起
先想象一个场景:公司有一块白板,上面写着各种任务安排。很多同事需要频繁查看白板(读操作),偶尔有经理需要修改任务(写操作)。
如果采用最简单粗暴的方式——每次查看或修改时都锁住整个白板,那么当多人同时查看时,大家只能排队等待,效率极低。这就像早期JDK中的Vector,虽然线程安全但性能不佳。
ArrayList更糟,它就像完全不管理白板使用,当多人同时读写时,内容可能变得混乱不堪。
那么,有没有一种机制,可以让多人同时查看白板,且经理修改时也不影响其他人查看呢?答案是肯定的,这就是CopyOnWriteArrayList的妙处所在!
什么是CopyOnWriteArrayList?
CopyOnWriteArrayList是Java并发包(JUC)中提供的线程安全List实现。顾名思义,它的核心思想是“写时复制”:在修改列表时,不直接操作原数据,而是先将原数据复制一份,在副本上修改,再用修改后的副本替换原数据。
// 关键代码展示publicbooleanadd(Ee){finalReentrantLocklock=this.lock;lock.lock();// 写操作加锁try{Object[]elements=getArray();intlen=elements.length;Object[]newElements=Arrays.copyOf(elements,len+1);// 复制新数组newElements[len]=e;// 修改副本setArray(newElements);// 替换原数组returntrue;}finally{lock.unlock();}}简单吧?但背后的设计思想却非常精妙!
核心原理:写时复制机制
读写分离的艺术
CopyOnWriteArrayList的核心是读写分离:
- 读操作:完全无锁,多个线程可以并发读取
- 写操作:使用ReentrantLock保证线程安全,但每次写操作都会复制整个数组
这就像我们前面说的白板场景:经理要修改任务时,不是直接在原白板上修改,而是将白板内容复印一份,在复印件上修改,修改完成后再用新的复印件替换原白板。这样,其他同事在经理修改过程中仍然可以查看原白板内容,完全不受影响。
关键技术实现
volatile数组保证可见性
privatetransientvolatileObject[]array;使用volatile修饰数组引用,确保一旦数组被替换,其他线程能立即看到新数组。
ReentrantLock保证写操作原子性
所有写操作(add、set、remove等)都使用相同的Reentrant锁,确保同一时刻只有一个线程能执行写操作。
迭代器的快照特性
CopyOnWriteArrayList的迭代器基于创建迭代器时的数组快照,因此不会抛出ConcurrentModificationException异常。
深入源码:看看实际如何工作
添加元素的过程
以add方法为例,它的执行流程如下:
- 获取锁:保证同一时刻只有一个写线程
- 复制数组:创建原数组的副本,长度+1
- 修改副本:将新元素加入副本数组末尾
- 替换引用:将volatile数组指向新数组
- 释放锁:写操作完成
这个过程就像餐厅的菜单更新:当需要添加新菜品时,厨师不会直接在原菜单上涂改,而是印制新菜单,替换掉旧菜单。顾客在更新过程中仍可查看旧菜单,完全不受影响。
读取元素的极致简单
与写操作相比,读操作简单到令人发指:
publicEget(intindex){returnget(getArray(),index);}privateEget(Object[]a,intindex){return(E)a[index];}没有锁!没有同步!这就是为什么读性能如此高效的原因。
优缺点分析:没有银弹,只有合适场景
优势明显
- 极高的读取性能:读操作完全无锁,支持高并发读取
- 线程安全:写操作通过锁和复制机制保证线程安全
- 不会抛出ConcurrentModificationException:迭代器使用快照,遍历过程中不会因并发修改而异常
缺点不容忽视
- 内存占用大:写操作需要复制整个数组,内存占用为原数组的两倍
- 数据一致性弱:读操作可能无法立即看到最新的写操作结果,只能保证最终一致性
- 写性能差:数据量越大,写操作性能越低
实战应用场景
1. 事件监听器列表
在图形界面或观察者模式中,事件监听器的管理是典型的读多写少场景:
publicclassEventManager{privatefinalCopyOnWriteArrayList<EventListener>listeners=newCopyOnWriteArrayList<>();// 读多:事件触发时频繁遍历监听器publicvoidfireEvent(Eventevent){for(EventListenerlistener:listeners){// 无需加锁,高效遍历listener.onEvent(event);}}// 写少:监听器的注册和注销相对较少publicvoidaddListener(EventListenerlistener){listeners.add(listener);}}这种场景下,事件触发非常频繁(大量读操作),而监听器的变化相对较少(少量写操作),正是CopyOnWriteArrayList的用武之地。
2. 缓存系统
只读或读多写少的缓存系统也非常适合使用CopyOnWriteArrayList:
publicclassProductCatalog{privatevolatileCopyOnWriteArrayList<Product>hotProducts=newCopyOnWriteArrayList<>();// 高频读取:众多用户同时查询热销商品publicList<Product>getHotProducts(){returnnewArrayList<>(hotProducts);// 无需同步,极速读取}// 低频更新:定时更新热销商品列表publicvoidupdateHotProducts(List<Product>newProducts){hotProducts=newCopyOnWriteArrayList<>(newProducts);}}3. JDBC驱动注册
Java的DriverManager中就使用了CopyOnWriteArrayList来管理已注册的JDBC驱动:
// JDK中的实际应用publicclassDriverManager{privatefinalstaticCopyOnWriteArrayList<DriverInfo>registeredDrivers=newCopyOnWriteArrayList<>();// 驱动注册(写操作较少发生)publicstaticsynchronizedvoidregisterDriver(java.sql.Driverdriver){// ... 将驱动信息添加到registeredDrivers}// 驱动查找(读操作频繁发生)privatestaticvoidloadInitialDrivers(){// ... 遍历registeredDrivers}}与Vector的对比:为什么选择CopyOnWriteArrayList?
很多初学者会问:既然Vector也是线程安全的,为什么还要用CopyOnWriteArrayList?
关键在于锁的粒度:
- Vector:所有操作(包括读)都使用synchronized同步,相当于读写都加锁
- CopyOnWriteArrayList:只有写操作加锁,读操作完全无锁
在读多写少的场景下,这种差异导致的性能差距可能是数量级的!下面的表格直观对比了它们的差异:
| 特性 | Vector | CopyOnWriteArrayList |
|---|---|---|
| 读性能 | 差(全程加锁) | 极佳(完全无锁) |
| 写性能 | 一般 | 差(数据量大时) |
| 内存占用 | 正常 | 高(写时复制) |
| 数据一致性 | 强一致性 | 最终一致性 |
| 适用场景 | 读写均衡 | 读多写少 |
使用技巧与注意事项
- 适合数据量小的场景:数据量越大,写操作的成本越高
- 适合读多写少的场景:写操作越频繁,性能问题越明显
- 注意迭代器的弱一致性:迭代器反映的是创建时的快照,不是最新数据
- 批量写入优化:多次写操作可以合并为一次
// 不推荐:多次写操作,多次数组复制for(Stringitem:items){copyOnWriteList.add(item);}// 推荐:单次写操作,只需一次数组复制copyOnWriteList.addAll(Arrays.asList(items));总结
CopyOnWriteArrayList通过写时复制技术,以空间换时间,巧妙解决了读多写少场景下的并发性能问题。它就像一家聪明的餐厅:在更新菜单时不影响顾客点餐,在保证数据一致性的前提下极大提升了读取性能。
但是,没有万能的解决方案,CopyOnWriteArrayList在写多读少或数据量巨大的场景下并不适用。选择合适的工具解决特定问题,才是优秀开发的标志。
希望本文能帮助你深入理解CopyOnWriteArrayList,在下次面对并发读取挑战时,能够自信地选择最适合的方案!
参考文章
- https://bbs.huaweicloud.com/blogs/428120
- https://juejin.cn/post/7084053214412144676
- https://blog.csdn.net/qq_40395278/article/details/103899990
- https://blog.csdn.net/weixin_42073629/article/details/102493255
- https://developer.aliyun.com/article/1582941
- https://www.cnblogs.com/yashon/p/15007925.html
- https://juejin.cn/post/7277490240171802636
- https://blog.csdn.net/qq_39938758/article/details/103860594
以上就是关于CopyOnWriteArrayList的深入解析。如果有任何问题或见解,欢迎在评论区留言讨论!别忘了点赞和收藏哦~
更多技术干货欢迎关注微信公众号科威舟的AI笔记~
【转载须知】:转载请注明原文出处及作者信息