ArrayList在多线程调用Add()添加元素时的下标越界问题(java.lang.ArrayIndexOutOfBoundsException)
2016-04-27 10:24
483 查看
最近在看《实战Java虚拟机》一书,看到有关锁与并发章节时,看到如下一个多线程使用ArrayList的例子:
两个线程t1和t2同时向numberList中添加数据,由于ArrayList是线程不安全的,因此会导致添加的数据有错误,这个我还是能理解的,但是它报的确是如下错误:
我就有点理解不了了,ArrayList不是自动扩容、没有长度限制吗,为什么还会出现数组下标越界这种错误呢?
为了便于分析,我对代码进行了一点点修改:
执行结果为:
有时还会出现null,
带着种种不解,来看ArrayList添加流程:
首先,ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。
对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。
1、程序中报错的atjava.util.ArrayList.elementData(ArrayList.java:400)和atjava.util.ArrayList.add(ArrayList.java:441),它们同属Add()方法。
源码如下:
添加操作,首先会调用ensureCapacityInternal(size
+1),其作用为保证数组的容量始终够用,其中size是elementData数组中元组的个数,初始为0。
在ensureCapacityInternal()函数中,用if判断,如果数组没有元素,给数组一个默认大小,会选择实例化时的值与默认大小中较大值,然后调用ensure
两个线程t1和t2同时向numberList中添加数据,由于ArrayList是线程不安全的,因此会导致添加的数据有错误,这个我还是能理解的,但是它报的确是如下错误:
我就有点理解不了了,ArrayList不是自动扩容、没有长度限制吗,为什么还会出现数组下标越界这种错误呢?
为了便于分析,我对代码进行了一点点修改:
执行结果为:
有时还会出现null,
带着种种不解,来看ArrayList添加流程:
首先,ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。
对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。
1、程序中报错的atjava.util.ArrayList.elementData(ArrayList.java:400)和atjava.util.ArrayList.add(ArrayList.java:441),它们同属Add()方法。
源码如下:
添加操作,首先会调用ensureCapacityInternal(size
+1),其作用为保证数组的容量始终够用,其中size是elementData数组中元组的个数,初始为0。
在ensureCapacityInternal()函数中,用if判断,如果数组没有元素,给数组一个默认大小,会选择实例化时的值与默认大小中较大值,然后调用ensure
ExplicitCapacity()。
函数体中,modCount是数组发生size更改的次数。然后if判断,如果数组长度小于默认的容量10,则调用扩大数组大小的方法grow()。[/code]
函数grow()解释了基于数组的ArrayList是如何扩容的。数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。
接下来回到Add()函数,继续执行,elementData[size++]=e;这行代码就是问题所在,当添加一个元素的时候,它可能会有两步来完成:1.在elementData[Size]的位置存放此元素;2.增大Size的值。
在单线程运行的情况下,如果Size=0,添加一个元素后,此元素在位置0,而且Size=1;
而如果是在多线程情况下,比如有两个线程,线程A先将元素存放在位置0。但是此时CPU调度线程A暂停,线程B得到运行的机会。线程B也向此ArrayList添加元素,因为此时Size仍然等于0(注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加Size的值。那好,我们来看看ArrayList的情况,元素实际上只有一个,存放在位置0,而Size却等于2。这就是“线程不安全”了。这就解释了为何集合中会出现null。
但是数组下标越界还不能仅仅依靠这个来解释。我们观察发生越界时的数组下标,分别为10、15、22、33、49和73。结合前面讲的数组自动机制,数组初始长度为10,第一次扩容为15=10+10/2,第二次扩容22=15+15/2,第三次扩容33=22+22/2...以此类推,我们不难发现,越界异常都发生在数组扩容之时。
由此给了我想法,我猜想是,由于没有该方法没有同步,导致出现这样一种现象,用第一次异常,即下标为15时的异常举例。当集合中已经添加了14个元素时,一个线程率先进入add()方法,在执行ensureCapacityInternal(size+1)时,发现还可以添加一个元素,故数组没有扩容,但随后该线程被阻塞在此处。接着另一线程进入add()方法,执行ensureCapacityInternal(size+1),由于前一个线程并没有添加元素,故size依然为14,依然不需要扩容,所以该线程就开始添加元素,使得size++,变为15,数组已经满了。而刚刚阻塞在elementData[size++]=e;语句之前的线程开始执行,它要在集合中添加第16个元素,而数组容量只有15个,所以就发生了数组下标越界异常!
相关文章推荐
- Servlet Listener之ServletContextListener用法
- spring和mybatis 配置小知识点
- [转]如何设置eclipse中js默认打开为java Editor
- java时间转换
- 监听器---java
- Java设计模式--策略模式d
- 10 Exception handling Best Practices in Java Programming
- spring.Net之Ioc在MVC中的使用
- Java基础-01
- mybatis+springmvc 多数据源切换
- 深入理解Java:注解(Annotation)--注解处理器
- 【Spring】Spring MVC原理及配置详解
- Java序列化id
- Spring定时器--时间设置规则(转载)
- Java Integer 判等误区
- 策略模式--Java篇
- java异常体系
- Java多线程编程实战指南(设计模式篇,黄文海)-之管道线模式
- java中静态代码块的用法 static用法详解
- studio中java文件右下角显示红色j而不是C标识