arrayList是一个线程不安全的集合,在多线程情况下可能会引起数据不一致、数组越界等问题。下面具体列一下多线程情况下ArrayList会出现什么错误.
1.java.util.ConcurrentModificationException
ConcurrentModificationException 中文意思就是并发修改异常,存在于并发使用 Iterator 时出现的时候.
在多线程的情况下,一个线程在使用Iterator进行遍历,另一个线程往集合里面新增了一条数据,就有可能会出现这个错误.
原因:ArrayList中存放了一个属性modCount记录了集合被修改的次数,在创建迭代器(Iterator)时会将modCount赋值到迭代器的expectedModCount属性中,迭代器调用next()方法时判断modCoun是否等于expectedModCount,如果不等于则抛出ConcurrentModificationException异常。
迭代器next方法源码:
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
判断的方法是:checkForComodification
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
可以看出checkForComodification方法校验了两个属性是否相等,不相等会抛出异常。
多线程情况下出现创建完迭代器后,其他线程往集合里面添加了数据modCount加1,导致modCount与expectedModCount不相等,导致报错。
2.出现NULL问题和元素值覆盖
多线程情况下,会发现调用add方法没有传null的情况,但是输出集合内容时发现存在null值。
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 300; i++) {
new Thread(() -> {
if (list.isEmpty()) {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
list.iterator();
}
}, i + "").start();
}
这个问题是add方法引起的问题。
看下add方法的源码:
public boolean add(E e) {
//扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//赋值
elementData[size++] = e;
return true;
}
在网上搜索了一下相关问题,有的说是应为扩容所导致的,有的说是赋值使用size++导致的,经过分析个人任务是使用size++导致的。因为size++是非原子性的,多线程情况下会出现问题。
elementData[size++] = e;可为两步
1.给索引位置赋值
2.size加1
如果现在有两个线程a,b,size为5,a线程读到索引位值时,将值赋值到这个位置,这时cpu时间片让出,b线程读到的size也是5(因为a线程没有执行加1操作就将时间片让出来了),b线程将值赋值到size=5这个位置,然后size加1。这个时候a线程恢复cpu时间片,size再加1。这样,a线程和b线程都赋值到了size=5这个位置,索引位置值变成了a线程赋值的值,b线程的值被覆盖了,但是size却加了2次,最后size经过两次加1变成了7,size=6这个这个下标的值就会为null.
ArrayList底层的数据结构是数组,数组里面对应下标没有值就是null.
举例:
Integer[] elementData = new Integer[22];
int size = 0;
for (int i = 0; i< 10; i++) {
elementData[size] = i;
size = size + 2;
}
System.out.println(Arrays.toString(elementData));
3.数组下标越界问题
数组越界异常 ArrayIndexOutOfBoundsException
由于ArrayList添加元素是如上面分两步进行,可以看出第一个不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:
- 列表大小为9,即size=9
- 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
- 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
- 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
- 线程B也发现需求大小为10,也可以容纳,返回。
- 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
- 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.