敞开成长之旅!这是我参与「日新方案 12 月更文挑战」的第7天,点击查看活动概况
前几天刷博客时,无意中看到一篇名为《CopyOnWriteArrayList真的彻底线程安全吗》博客。心中不由泛起疑问,它便是线程安全的啊,难道还有啥特殊情况?
咱们知道CopyOnWrite
的中心思想正如其名:写时复制
。在对数据有修正操作时,先复制再操作,最终替换原数组。在这些操作时,是有加锁的了。
1 问题复现
这篇博文中主要提到数组越界
反常。场景为:假设现在有一个已存在的列表,线程1测验去查询列表最终一个元素,而此刻线程2要去删去列表最终一个元素。此刻线程1由于最开端读取的size()=n,在线程2删去后size()=n-1,再拿原Index办法时,便触发ArrayIndexOutOfBoundsException
反常。
其实读到这儿,咱们就现已知道了问题所在。在读取列表巨细
和根据索引拜访
两个时间点,列表数据现已发生了改变。这种反常理论上归于可预知的反常。
请看下面的代码,并思考下并发履行会有问题吗
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>(l);
while (true) {
if (!cowList.isEmpty()) {
cowList.remove(0);
} else {
return;
}
}
咱们无妨来试下。
/**
* @author lpe234
* @date 2022/12/03
*/
@Slf4j
public class CowalTest {
public static void main(String[] args) {
List<String> l = new ArrayList<>();
for (int i = 0; i < 100; i++) {
l.add(String.valueOf(i));
}
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>(l);
final Runnable rab = () -> {
while (true) {
if (!cowList.isEmpty()) {
cowList.remove(0);
} else {
return;
}
}
};
new Thread(rab).start();
new Thread(rab).start();
}
}
程序履行结果如下:
Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at java.base/java.util.concurrent.CopyOnWriteArrayList.elementAt(CopyOnWriteArrayList.java:386)
at java.base/java.util.concurrent.CopyOnWriteArrayList.remove(CopyOnWriteArrayList.java:478)
at com.example.other.CowalTest.lambda$main$0(CowalTest.java:25)
at java.base/java.lang.Thread.run(Thread.java:834)
原因就在于cowList.isEmpty()
和cowList.remove(0)
为两个操作。在这两个操作之间,并没有什么机制来保证cowList
不会改变。所以呈现反常,是可预见的。
2 源码剖析
中心属性及get/set办法。
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** 一切涉及到array改变操作的锁。(在内置锁和ReentrantLock都可运用时,咱们更倾向于内置锁) */
final transient Object lock = new Object();
/** 这个数组的一切拜访,只会经过getArray/setArray来进行。 */
private transient volatile Object[] array;
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
可见实现其实很简单。内部运用Object[] array
来承载数据。运用volatile
来保证多线程下数组的可见性。
再看下isEmpty
和remove
办法。
public int size() {
return getArray().length;
}
public boolean isEmpty() {
return size() == 0;
}
public E remove(int index) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}
可以很清晰的看到,在这俩办法中,均有getArray()
调用。假如中间呈现其他线程修正数据,这俩数据必然不一致。在看一个add(E e)
办法。
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
此刻咱们可以很清晰的看清他的编程逻辑。
- 但凡对数组有修正的操作,先获取锁。
- 经过
getArray()
获取数据。前面已加锁,为最新数据,在开释锁前不会有其他线程修正。 - 对数据进行相关修正操作,
Arrays.copyOf
是重点。 - 经过
setArray(es)
将修正后的数据赋值给原数组。 - 开释锁。
3 思考
3.1 经过本例咱们能学到什么
- 相似
CopyOnWriteArrayList
这种并发安全的类,假如不合理(不标准的、过错的)的运用,也会导致并发安全问题 - 面对事物,要知其然知其所以然。只要了解内部原理,才干更好的去运用它。
- 在
CopyOnWriteArrayList
代码中可以看到,当遇到修正操作时,基本都离不开Arrays.copyOf
,这种复制会占用额外一倍的内存空间。假如有很多频繁的修正操作,显然是不太合适的。 - 在修正相关操作代码逻辑中,可以体会到,全体是有那么一点点的推迟的。即一个线程修正完并setArray后,另外的线程才干获取到最新值。
3.2 其他的呢
-
CopyOnWrite
是一种很好的思想,它可以使读、写
操作并发履行。在Redis的RDB快照生成时,也运用了该思想。 - 为什么会有
final transient Object lock = new Object()
这个锁?假如细心看过源码就能明白,其实便是最大程度的减少锁的范围(粒度)。
public boolean addAll(Collection<? extends E> c) {
Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
if (cs.length == 0)
return false;
synchronized (lock) {
// 略...
}
}
echo '5Y6f5Yib5paH56ugOiDmjpjph5Eo5L2g5oCO5LmI5Zad5aW26Iy25ZWKWzkyMzI0NTQ5NzU1NTA4MF0pL+aAneWQpihscGUyMzQp' | base64 -d