大家好,我是walker
一个从文科自学转行的程序员~
爱好编程,偶尔写写编程文章和生活
欢迎关注公众号【I am Walker】,回复“电子书”,就可以获得200多本编程相关电子书哈~
我的gitee:https://gitee.com/shen-chuhao/walker.git 里面很多技术案例!
参考:CopyOnWriteArrayList真的完全线程安全吗
CopyOnWriteArrayList 是什么?
- CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。
- CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。
- 在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
CopyOnWriteArrayList不安全案例
数组越界
如果这时候有第三个线程进行删除元素操作,读线程去读取容器中最后一个元素,读之前的时候容器大小为i,当去读的时候删除线程突然删除了一个元素,这个时候容器大小变为了i-1,读线程仍然去读取第i个元素,这时候就会发生数组越界。
package concurrentContainer.copyOnWrite;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteTest {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
/**
* 主线程往copyOnWrite数组中加入数据
*/
for(int i = 0; i<5000; i++){
list.add("a" + i);
}
/**
* 该线程一直获取数组的最后一个元素
*/
new Thread(()->{
while (true) {
if (list.size() > 0) {
String content = list.get(list.size() - 1);
}else {
break;
}
}}).start();
/**
* 该数组一直移除元素
*/
new Thread(()->{
while (true) {
if(list.size() <= 0){
break;
}
list.remove(0);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
输出结果: 抛出异常
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 4993
at java.util.concurrent.CopyOnWriteArrayList.get(CopyOnWriteArrayList.java:388)
at java.util.concurrent.CopyOnWriteArrayList.get(CopyOnWriteArrayList.java:397)
at concurrentContainer.copyOnWrite.CopyOnWriteTest.lambda$main$0(CopyOnWriteTest.java:26)
at java.lang.Thread.run(Thread.java:748)
因此可以知道,CopyOnWriteArrayList 的使用场景是读多写少的场景
有哪些优缺点?
优点
就是合适读多写少的场景。
缺点
消耗内存 不能满足实时性 代价大
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
- 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
CopyOnWriteArrayList 的设计思想 3`
- 读写分离,读和写分开
- 最终一致性
- 使用另外开辟空间的思路,来解决并发冲突
源码解析
add
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//复制数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将元素加载最后一个位置
newElements[len] = e;
//将新复制的数组设置回去
setArray(newElements);
return true;
} finally {
//解锁
lock.unlock();
}
}
setArray
final void setArray(Object[] a) {
array = a;
}