当前位置: 首页 > 后端技术 > Java

CopyOnWriteArrayList真的是线程安全的吗?

时间:2023-04-01 19:15:30 Java

前几天浏览博客的时候无意中看到了一篇名为《CopyOnWriteArrayList真的完全线程安全吗》的博客。心里不禁产生了疑问,是线程安全的,有什么特殊情况吗?我们知道,CopyOnWrite的核心思想就像它的名字一样:copyonwrite。修改数据时,先复制再操作,最后替换原数组。在这些操作期间,有一个锁。1问题复现这篇博文主要提到数组越界异常。场景是:假设现在有一个存在的列表,线程1试图查询列表的最后一个元素,此时线程2要删除列表的最后一个元素。此时线程1一开始读取size()=n,线程2删除size()=n-1后,再使用原来的Index方法,触发ArrayIndexOutOfBoundsException。其实看了这里,我们就已经知道问题所在了。在读取列表大小和根据索引访问这两个时间点,列表数据发生了变化。这种异常在理论上是可预测的异常。请看下面的代码,想想并发执行会不会有问题0);}else{返回;}}让我们试一试。/***@authorlpe234*@date2022/12/03*/@Slf4jpublicclassCowalTest{publicstaticvoidmain(String[]args){Listl=newArrayList<>();对于(inti=0;i<100;i++){l.add(String.valueOf(i));}CopyOnWriteArrayListcowList=newCopyOnWriteArrayList<>(l);finalRunnablerab=()->{while(true){if(!cowList.isEmpty()){cowList.remove(0);}else{返回;}}};新线程(rab).start();新线程(rab).start();}}程序执行结果如下:线程"Thread-1"中的异常java.lang.ArrayIndexOutOfBoundsException:Index0outofboundsforlength0atjava.base/java.util.concurrent.CopyOnWriteArrayList.elementAt(CopyOnWriteArrayList.java:386)在java.base/java.util.concurrent.CopyOnWriteArrayList.remove(CopyOnWriteArrayList.java:478)atcom.example.other.CowalTest.lambda$main$0(CowalTest.java:25)atjava.base/java.lang.Thread.run(Thread.java:834)原因在于cowList.isEmpty()和cowList.remove(0)是介于这两个操作之间的两个操作,没有机制保证cowList不会改变。所以异常是可以预测的。2源码分析核心属性和get/set方法。公共类CopyOnWriteArrayList实现List,RandomAccess,Cloneable,java.io.Serializable{privatestaticfinallongserialVersionUID=8673264195747942595L;/**数组更改操作涉及的所有锁。(当内置锁和ReentrantLock都可用时,我们更喜欢内置锁)*/finaltransientObjectlock=newObject();/**对该数组的所有访问只能通过getArray/setArray完成。*/privatetransientvolatileObject[]数组;/***获取数组。非私有以便也可以从CopyOnWriteArraySet类访问*。*/finalObject[]getArray(){返回数组;}/***设置数组。*/finalvoidsetArray(Object[]a){array=a;可以看出实现其实很简单。Object[]数组在内部用于承载数据。使用volatile保证数组在多线程下的可见性。再次查看isEmpty和remove方法。publicintsize(){returngetArray().length;}publicbooleanisEmpty(){returnsize()==0;}publicEremove(intindex){synchronized(lock){Object[]es=getArray();intlen=es.length;EoldValue=elementAt(es,index);intnumMoved=len-索引-1;对象[]新元素;如果(numMoved==0)newElements=Arrays.copyOf(es,len-1);else{newElements=newObject[len-1];System.arraycopy(es,0,newElements,0,index);System.arraycopy(es,index+1,newElements,index,numMoved);}setArray(新元素);返回旧值;}}可以清楚的看到,在这两个方法中,都有getArray()的调用。如果中间有其他线程修改了数据,这两个数据肯定是不一致的。查看add(Ee)方法。publicbooleanadd(Ee){synchronized(lock){Object[]es=getArray();intlen=es.length;es=Arrays.copyOf(es,len+1);es[len]=e;设置数组(es);返回真;}}到这里我们就可以清楚的看到他的编程逻辑了。任何修改数组的操作首先获取锁。通过getArray()获取数据。之前已经加锁了,而且是最新的数据,在释放锁之前不会有其他线程修改它。对于数据的相关修改操作,Arrays.copyOf是重点。通过setArray(es)将修改后的数据赋值给原数组。释放锁。3思考3.1通过这个例子,我们可以了解到像CopyOnWriteArrayList这样并发安全的类有哪些?所以是的。只有了解了内部原理,才能更好地使用它。在CopyOnWriteArrayList代码中可以看出,遇到修改操作时,基本离不开Arrays.copyOf。该副本将额外占用一倍的内存空间。如果有大量频繁的修改操作,显然不适合。在修改相关操作代码的逻辑时,可以发现整体有一点延迟。即一个线程修改和setArray后,其他线程可以拿到最新的值。3.2其他CopyOnWrite是一个很好的想法,它可以使读写操作并发执行。Redis的RDB快照生成时也用到了这个思路。为什么会有一个finaltransientObjectlock=newObject()lock?仔细看源码就可以明白,其实是最大程度的降低了锁的作用范围(粒度)。publicbooleanaddAll(Collectionc){Object[]cs=(c.getClass()==CopyOnWriteArrayList.class)?((CopyOnWriteArrayList)c).getArray():c.toArray();如果(cs.length==0)返回false;synchronized(lock){//略...}}echo'5Y6f5Yib5paH56ugOiDmjpjph5Eo5L2g5oCO5LmI5Zad5aW26Iy25ZWKWzkyMzI0NTQ5NzU1NTA4MF0pL+aAneWQpihscGUyMzQp'|base64-d