java多线程异步操作ArrayList出现数组越界异常问题
今天想通过多线程向集合中添加元素来测试线程池的性能时,想当然的使用了List的ArrayList的实现类。结果出现了数组越界异常,知道ArrayList底层是数组实现的小伙伴可能会认为难道ArrayList也有容量上限??可是我的测试也就100000个,所以不可能是达到ArrayList的上限问题。后来发现经常记的"ArrayList是线程不安全的"那句八股文真的是在自己脑海里就是一段有记忆但没有用的废h话。。。。。
所以此次线程操作ArrayList引发的数组越界异常就是==“ArrayList线程不安全”==引起的。那为什么ArrayList为什么是线程不安全的呢?这还要和ArrayList扩容机制说起。关于ArrayList的扩容机制请看我的这篇文章
我们都知道ArrayList底层是数组,那我们来看看源码中这个数组是如何定义的。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; //transient是防止字段序列化
}
数组在构造器中初始化
//有参
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//无参
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
显而易见,数组在初始定义时就没有添加一些同步关键字(如:synchronized)进行限制。这还不是最致命的,来看看它的扩容方法(扩容的逻辑代码段比较长,这里裁剪了比较重要的一部分)。
//以下两个方法是扩容机制的逻辑
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//下面的方法是扩容的实现
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
从扩容逻辑到扩容实现可见,所有的方法都没有添加线程同步限制。所以在多线程异步操做ArrayList时,由于不同线程中扩容逻辑代码段中的参数不统一,造成了数组该扩容的时候没有实时扩容,造成了ArrayList底层维护的数组发生了越界。这也是问题的出现。
知道了问题存在,解决方法也就出来了,换个List接口的实现类。但是后来想一想使用LinkList实现类可以吗?该实现类底层是基于链表实现的,不存在数组下标越界异常,此时八股文"LinkedList也是线程不安全的"缓缓在脑中飘过。。。。
public class Test1 {
public static void main(String[] args) {
//List<Integer> list = new ArrayList<>();
List<Integer> list = new LinkedList<>();
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 100000; i++) {
threadPool.execute(new MThread(list));
}
System.out.println("集合中的个数为: " + list.size());
threadPool.shutdown();
}
}
class MThread implements Runnable {
static Random random = new Random();
private List<Integer> list;
public MThread(List<Integer> list) {
this.list = list;
}
@Override
public void run() {
list.add(random.nextInt());
}
}
//运行结果:集合中的个数为: 92979
但我还是想试一试会发生什么,于是将List的实现类换成了LinkedList,结果瞬间让我拉回了刚学线程的那会,集合中的数据个数没有达到预期值。so….这个集合的实现类必须满足线程安全。所以这里改用Vector(由于加锁导致性能降低,在不需要并发访问同一对象时,这种强制性的同步机制就显得多余,所以现在Vector已被弃用)。问题解决!
总结:实践出真知,八股文有时候可以帮我们快速定位到bug,但是只是一味的记忆不去联想应用于实际,就等于自己记忆了一段废话。