一、写在开头
我们在学习集合或者说容器的时候了解到,很多集合并非线程安全的,在并发场景下,为了保障数据的安全性,诞生了并发容器,广为人知的有ConcurrentHashMap、ConcurrentLinkedQueue、BlockingQueue等,那你们知道ArrayList也有自己对应的并发容器嘛?
作为使用频率最高的集合类之一,ArrayList线程不安全,我们在并发环境下使用,一般要辅以手动上锁、或者通过Collections.synchronizedList()转一手,为了解决这一问题,Doug Lea(道格.利)大师为我们提供了它的并发类——CopyOnWriteArrayList
。
二、认知CopyOnWriteArrayList
CopyOnWriteArrayList 是java.util.concurrent的并发类,线程安全,遵循写时复制的原则(CopyOnWrite),什么意思呢?就是我们在对列表进行增删改时,会先创建一个列表的副本,在副本中完成增删改操作后,再将副本替换原列表,整个过程旧的列表并没有锁定,因此原来的读取操作仍可继续。
看到这里细心的同学应该已经发现了它的“弊端”了,先赋值副本,写完再替换,这是有时间差的,没错,这就是CopyOnWrite的延时更新策略,我们在发生写的同时,不阻塞读,但读取的只是旧列表中的数据,直到引用替换完成,可以保证数据的最终一致性,无法保证实时性。
三、实现原理(源码)
我们来看一下CopyOnWriteArrayList底层的源码实现,首先在内部维护了一个数组,用volatile关键字修饰,保证了数据的内存可见性
/** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array;
读取:get()方法
public E get(int index) { return get(getArray(), index); } /** * Gets the array. Non-private so as to also be accessible * from CopyOnWriteArraySet class. */ final Object[] getArray() { return array; } private E get(Object[] a, int index) { return (E) a[index]; }
这段源码没什么,很好理解,就是普通的读取数组的操作,这也能看出CopyOnWriteArrayList的读是不阻塞的。
新增:add()方法
public boolean add(E e) { final ReentrantLock lock = this.lock; //1. 使用Lock,保证写线程在同一时刻只有一个 lock.lock(); try { //2. 获取旧数组引用 Object[] elements = getArray(); int len = elements.length; //3. 创建新的数组,并将旧数组的数据复制到新数组中 Object[] newElements = Arrays.copyOf(elements, len + 1); //4. 往新数组中添加新的数据 newElements[len] = e; //5. 将旧数组引用指向新的数组 setArray(newElements); return true; } finally { lock.unlock(); } } final void setArray(Object[] a) { array = a; }
通过这段源码,我们就能够感知到前面描述的实现原理了,首先,新增元素时,内部通过可重入锁进行锁定,说明写时会独占,然后,再将原数组赋值到一个新数组中,最后,将旧数组的引用指向新数组。
四、使用注意事项,用不好坑死你
对于CopyOnWriteArrayList的日常使用,和ArrayList几乎一模一样,在这里就不用过多介绍了,但它的使用还是需要注意的,虽然可以保证线程安全,但因其特性所致,仅适应于读多写少的并发环境,对于频繁写入或者写入的对象较大,一定不要使用CopyOnWriteArrayList容器,不然会坑死你的!
【举个例子】
之前在这篇文章中:[EasyExcel导入导出百万数据量]
采用了CopyOnWriteArrayList,以此来保证在多线程写入数据库时的线程安全,由于写入的excel文件中有100万的数据量,再导入的时候非常之慢,用了514秒!
核心实现代码如下,具体内容实现可去看那篇文章哈
@Slf4j @Service public class EasyExcelImportHandler implements ReadListener<User> { /*成功数据*/ private final CopyOnWriteArrayList<User> successList = new CopyOnWriteArrayList<>(); /*单次处理条数*/ private final static int BATCH_COUNT = 20000; @Resource private ThreadPoolExecutor threadPoolExecutor; @Resource private UserMapper userMapper; @Override public void invoke(User user, AnalysisContext analysisContext) { if(StringUtils.isNotBlank(user.getName())){ successList.add(user); return; } if(successList.size() >= BATCH_COUNT){ log.info("读取数据:{}", successList.size()); saveData(); } } /** * 采用多线程读取数据 */ private void saveData() { List<List<User>> lists = ListUtil.split(successList, 20000); CountDownLatch countDownLatch = new CountDownLatch(lists.size()); for (List<User> list : lists) { threadPoolExecutor.execute(()->{ try { userMapper.insertSelective(list.stream().map(o -> { User user = new User(); user.setName(o.getName()); user.setId(o.getId()); user.setPhoneNum(o.getPhoneNum()); user.setAddress(o.getAddress()); return user; }).collect(Collectors.toList())); } catch (Exception e) { log.error("启动线程失败,e:{}", e.getMessage(), e); } finally { //执行完一个线程减1,直到执行完 countDownLatch.countDown(); } }); } // 等待所有线程执行完 try { countDownLatch.await(); } catch (Exception e) { log.error("等待所有线程执行完异常,e:{}", e.getMessage(), e); } // 提前将不再使用的集合清空,释放资源 successList.clear(); lists.clear(); } /** * 所有数据读取完成之后调用 * @param analysisContext */ @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { //读取剩余数据 if(CollectionUtils.isNotEmpty(successList)){ log.info("读取数据:{}条",successList.size()); saveData(); } } }
而将这段代码中的CopyOnWriteArrayList换为ArrayList。
/*成功数据*/ // private final CopyOnWriteArrayList<User> successList = new CopyOnWriteArrayList<>(); private final List<User> successList = new ArrayList<>();
导入100万数据量的耗时,直接从分钟降为秒级,由此可见CopyOnWriteArrayList在写入大对象时的性能非常之差!
五、总结
通过以上的学习,我们进行总结:CopyOnWriteArrayList的优势在于可以保证线程安全的同时,不阻塞读操作,但是这仅限于读多写少的情况;
在写多读少的情况下,或者写入的对象占用内容较大时,不建议使用CopyOnWriteArrayList;CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用 CopyOnWrite 容器,最好通过 ReentrantReadWriteLock 自定义一个的列表。
六、结尾彩蛋
如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)