java 多线程数组越界_ArrayList在多线程调用Add()添加元素时的下标越界问题(java.lang.ArrayIndexOutOfBoundsException)...

本文分析了在多线程环境下,如何由于ArrayList的非线程安全特性,导致添加元素时出现`ArrayIndexOutOfBoundsException`。通过示例代码展示了在并发情况下,多个线程同时尝试添加元素可能造成的问题,特别是在数组扩容时,可能出现的一个线程完成了部分添加操作,但未更新size,而另一个线程紧接着尝试添加元素,导致下标越界。通过对ArrayList的add方法和扩容机制的解析,揭示了问题的根本原因。
摘要由CSDN通过智能技术生成

标签:

最近在看《实战Java虚拟机》一书,看到有关锁与并发章节时,看到如下一个多线程使用ArrayList的例子:

72bba955441169de29e0a976cf300870.png

ee7bdb25e72a174db9d42f1efc0bb66f.png

两个线程t1和t2同时向numberList中添加数据,由于ArrayList是线程不安全的,因此会导致添加的数据有错误,这个我还是能理解的,但是它报的确是如下错误:

87d72e9fd2af3498d9d51fe98f11162b.png

我就有点理解不了了,ArrayList不是自动扩容、没有长度限制吗,为什么还会出现数组下标越界这种错误呢?

为了便于分析,我对代码进行了一点点修改:

b6b2da42182670c8e7862a8132b816ef.png  执行结果为:

e3a710dc0c53439e41427b27be52a3d9.png

6ddcf55af67bb6896cf72c8b0a1c34ba.png

1bc84bcc2a261405a31ac42b3226acce.png

a66801b84ead73ad1860b79a276b828b.png

6b945c42649ad066631d0814276ebf57.png

31208593f7d21f1816d46f3a0f672f5a.png

有时还会出现null,

08d1a9d2ea2edcf5b1443cead61b70d5.png

带着种种不解,来看ArrayList添加流程:

首先,ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。

对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。

1、程序中报错的 at java.util.ArrayList.elementData(ArrayList.java:400) 和 at java.util.ArrayList.add(ArrayList.java:441),它们同属Add()方法。

源码如下:

08e469f9fcc85c32a1fda9fa6a971b0a.png

添加操作,首先会调用ensureCapacityInternal(size

+ 1),其作用为保证数组的容量始终够用,其中size是elementData数组中元组的个数,初始为0。

620c507fd439a33af2e03922539403fd.png

在ensureCapacityInternal()函数中,用if判断,如果数组没有元素,给数组一个默认大小,会选择实例化时的值与默认大小中较大值,然后调用ensureExplicitCapacity()。

b66eb57f17d3b9d0bf5cd86c385697c1.png

函数体中,modCount是数组发生size更改的次数。然后if判断,如果数组长度小于默认的容量10,则调用扩大数组大小的方法grow()。

f0c4db85a95b03eb7965a752af697c6b.png

函数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个,所以就发生了数组下标越界异常!

标签:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值