CopyOnWriteArrayList
查看博客和知乎得出的很多CopyOnWriteArrayList的特性.还有它的优缺点.
优点:
可以在多线程环境下操作 List
读的效率很高
不会抛ConcurrentModificationException
CopyOnWrite的缺点
CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。
内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。【当执行add或remove操作没完成时,get获取的仍然是旧数组的元素】
那我们从源码中来解析看看到底是什么原因会有导致有这些优点和缺点:
这里显示的是构造方法:
这里是有参add方法源码:
从add方法的源码中我们可以知道,
每一次添加一个值,都会上锁:
优点:保证了多线程下安全. 缺点:插入数据的速度会降低.
这里在没有copyOf之前获取的都是旧的数据:也就知道了数据不能实时获取.调用copyOf方法len+1,也就明白了为什么会消耗内存的缺点.
get方法:
从get方法源码中,我们可以看到,读数据的时候确实没有加锁.
set方法:
在这里很疑惑为什么需要再次setArray(elements).而不直接就返回.
如注释说明,是为了确保维持 volatile 的写语义,依据JSR-133的说明,4种读写内存屏障保障可见与禁止指令重排,可理解为,
1、在外部调用者调用 set(int index, E element) 方法时,如果被写入元素无变化,也保持一次对 volatie 修饰的 array 写操作,让JVM执行编译的指令时,外部调用者在 调用 set(int index, E element) 所操作的 Object[] array ,始终能通知到其他核心的本地缓存中 array 过期,重新从主存和L3中同步读取,保证读的可见;
2、同时,也会因这个写操作有 Lock 前辍,使得 set(idx, elm) 前后代码行不会发生 cpu 指令重排,保证了前后读写操的各种内存屏障下,使编译执行的指令逻辑顺序与代码逻辑期望一致。
在JDK11中这个已经排除掉了:
remove方法源码:
底层调用的就是remove(Object o, Object[] snapshot, int index)方法:
具体remove源码:
这里从新获取数组是为了并发编程,在此前的获取index位置的时候,其他并发操作已经删除或改变了地址情况.这里的逻辑思路.
获取底层数组和数组长度
判断如果参数的快照数组和刚获取的底层数组不一致 就进入一个名为findIndex的代码块
取得参数下标和刚获取的数组长度的最小值 进入for循环从0遍历至这个最小值之间的所有下标 判断如果有一个当前底层数组和参数快照数组有一个下标处元素不一致 但当前底层数组的该下标处元素和参数o一致 就将参数index赋为这个下标 这说明发生了并发修改 要删除的元素换了位置 然后跳出findIndex代码块
判断如果参数index大于等于刚获取的当前数组长度 就返回false
判断如果当前数组的index处元素为参数o 也跳出findIndex代码块
如果还没跳出代码块 就调用indexOf方法将参数o 当前数组 参数index和当前数组长度传进去 将返回值赋给index 再判断如果index小于0 就返回false 说明没找到这个元素
结束findIndex代码块后
创建新的Object数组长度为当前数组长度减一
将index之前的元素和index之后的元素拷贝进新数组 将新数组设为新的底层数组 然后返回true
最后解锁