关于ArrayList多线程并发下不安全的思考
首先考虑ArrayList单线程容易出现的异常,其实也就是迭代遍历的时候会出现异常
单线程
public class test1 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
// 初始化 list 集合里面的值
for (int i = 0; i < 10; i++) {
list.add(Integer.valueOf(i));
}
// 开始遍历 list 集合
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
Integer next = iterator.next();
if(next.intValue() == 6){
/**
* 删除值为 6 的 list 集合中的下标
* 这里首先通过 list 集合删除
*/
list.remove(next);
}
}
}
}
- 出现以下异常:
-
这里的原因如下:
-
首先弄清楚这几个变量的意思:
-
modCount
这个变量是继承自父类AbstractList,我们看源码发现每次的增加或者删除元素都会使这个变量++[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vIPNOLSh-1621257789832)(并发类.assets/image-20210517200927492.png)]
-
在ArrayLIst中有一个成员内部类Iterator,这个类中初始化了三个变量:
-
cursor
:下一个迭代元素的下标索引 -
lastRet
:最后一个元素下标索引 -
expectedModCount
:期望被修改的次数,等于 list 集合的修改次数
-
-
通过 list 集合删除元素会调用
fastRemove(index);
这个方法
因为修改了 list 集合,所以会使modCount++
,此时总共对list集合修改了11次,所以modCount应该为11,然而我们在遍历 list 集合的时候 Integer next = iterator.next();
这个步骤会调用一个方法checkForComodification()
,这个方法回去判断modCount != expectedModCount
,不等于就会抛出这个异常了。此时modCount 1
=11,expectedModCount
=10.
至于这种的解决方法,查了一下网上的说法,大多数都是用迭代器进行删除,即iterator.remove();
,确实,这种就是在删除的时候会同时改变expectedModCount
的值,这样就不会出这个异常了,但也只能删除,而没有添加的方法。
多线程
- 代码:
public class test2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 创建10个线程,往 list 里面添加元素
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,2));
/**
* 打印出 list 集合
* 出现异常 ConcurrentModificationException
* 这里如果不打印其实不会出现异常,但是仍然还是并发问题
*/
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
- 首先分析多线程操作ArrayList的并发问题,目前我遇到的只有一种情况,还有一种网上看到的:
-
数组下标越界(没遇到)
ArrayList的默认容量是10
size
:从注释中可以看到size是集合中包含元素的个数这种情况就是将设此时的size=9,假设有两个线程A,B同时对这个集合进行添加元素,其实
ensureCapacityInternal(size + 1); // Increments modCount!!
这个方法就是判断要不是将集合中的数组进行扩容,那此时size=9显然不用扩容,假设A,B两个线程同时判断,没有对数组进行扩容的话,那么数组的length就为10,那么A先添加元素,再size++
,此时size=10,然后B再添加元素,此时elementData[10],数组下标就越界了。 -
添加的实际元素没有 size() 大小的个数
这种并发问题分析为假设此时又A,B两个线程同时添加元素,假设size=0,那么还是add()
方法那里有问题
这一步操作不是原子性的,可以拆分为
elementData[size] = e;
size++;
如果在单线程下这个没有什么问题,但在多线程下就会出现并发问题,假设此时A添加完元素A但还没有执行size++
,然而B有添加了元素B,此时A,B两个线程都是在element Data[0]
这个位置添加了元素,但是A线程执行size++,B线程执行size++,那么size就变成了2。
// 理想状态下
elementData[0]=A;
elementData[1]=B;
size=2;
// 实际上却是
elementData[0]=B;
size = 2;
所以就出现了数组中的元素根本没有数组.size()
那么多个元素的并发问题。
那么为什么还是会出现ConcurrentModificationException
这个异常,哈哈哈,这里想了很久,其实就是在打印的时候会调用list集合的迭代器进行遍历,遍历的时候还是会执行Integer next = iterator.next();
方法,然后checkForComodification()
会判断modCount != expectedModCount
,假设此时线程A添加完元素之后就打印遍历集合,当遍历到modCount != expectedModCount
这里的判断的时候,又有一个线程B对集合进行了修改,那么,modCount++,所以会出现这个异常。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
- 解决这个异常的方法就是使用JUC并发包下的
CopyOnWriteArrayList
public class test3 {
public static void main(String[] args) {
// 使用 CopyOnWriteArrayList (写入时复制)
List<String> list = new CopyOnWriteArrayList<>();
// 创建10个线程,往 list 里面添加元素
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,2));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
可以看到这个类的add()方法是加了可重入锁ReentrantLock
的,也就保证了对集合进行修改的时候是多线程并发安全的了。
这个类也叫写入时复制,就是说在添加元素的时候,不是对原有的集合直接进行添加,而是先复制一个比原集合数组长度+1的数组,然后再对新数组进行添加,最后再将新数组设置为集合的数组。其实这样就保证了其他线程在读取这个集合的时候是读取到的原来的集合,因为对集合的读取不是线程同步的。
但这也有一个弊端,就是其他线程读取到的集合元素不是最新的。
这个类也叫写入时复制,就是说在添加元素的时候,不是对原有的集合直接进行添加,而是先复制一个比原集合数组长度+1的数组,然后再对新数组进行添加,最后再将新数组设置为集合的数组。其实这样就保证了其他线程在读取这个集合的时候是读取到的原来的集合,因为对集合的读取不是线程同步的。
但这也有一个弊端,就是其他线程读取到的集合元素不是最新的。