无法获取未定义或null_ArrayList并发写出现Null值

ArrayList并非线程安全的容器,这一点大家可能都非常清楚,但是在并发写入的情况下,不安全的情况具体有哪些,大家是否很清楚呢?本篇文章重点聊一下出现null的情况,然后对于其他并发写的安全做一个简单的叙述

我们看下面的代码,打印List的元素数量以及打印存储的元素

        List list = new ArrayList<>();        for (int i=0;i<10;i++) {            int finalI = i;            new Thread(()->{                list.add(finalI +1);            }).start();        }        System.out.println(list.size());        System.out.println(list.toString());

最理想的情况下,打印结果应该如下:

10[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

但是有可能出现一些其他问题,就像下面结果List元素出现null值的结果

10[null, 1, 3, 4, 5, 6, 7, 8, 9, 10]或者10[null, 2, 3, 4, 5, 6, 7, 8, 9, 10]或者10[null, null, null, 1, 5, 6, 7, 8, 9, 10]......

在我看百度看到的所有答案中,关于并发写出现Null值,几乎都是将原因归咎到add方法中的size++上,这里我个人认为这种回答应该是错误的,出现null值的原因应该是扩容所造成的。

 public boolean add(E e) {        ensureCapacityInternal(size + 1);        elementData[size++] = e;}

首先说一下为什么我觉得网上的答案是错误的,我们模拟add方法,然后使用javap命令拿到class的字节码看一下:

#### Java程序int size = 0;int[] elementDate = new int[5];public void add() {        elementDate[size++] = 10;}#### Javap 得到的字节码    public void add();  Code:       0: aload_0       1: getfield      #3 // Field elementDate:[I       4: aload_0       5: dup       6: getfield      #2// Field size:I       9: dup_x1      10: iconst_1      11: iadd      12: putfield      #2// Field size:I      15: bipush        10      17: iastore      18: return

在add方法的字节码中,通过getfield拿到elementDate数组放入栈顶(操作数栈),然后dup命令复制栈顶的数组并将复制值压入栈顶,然后再通过getfield获取size数值,下一步dup_x1命令会将栈顶的数值size复制两份,并将两个复制值压入栈顶,然后iconst_1命令将数值1压入栈顶,再使用iadd命令对栈顶的两个元素进行相加,并通过putfield将size更新,最后iastore更新数组(因为dup_x1复制了两份,所以数组的索引仍然是更新前的size)。大家可以好好想一下这个操作,无论size++多么不安全,因为索引复制两份被保存的操作数栈中,所以不可能在list中出现null值,只会出现覆盖的可能。

如果大家理解了上面的过程,我们思考下为什么null值出现了呢?由于ArrayList是基于数组实现,由于数组大小一旦确定就无法更改,所以其每次扩容都是将旧数组容器的元素拷贝到新大小的数组中(Arrays.copyOf函数),由于我们通过new ArrayList<>()实例的对象初始化的大小是0,所以第一次插入就会扩容,由于ArrayList并非线程安全,第二次插入时,第一次扩容可能并没完成,于是也会进行一次扩容(第二次扩容),这次扩容所拿到list的elementDate是旧的,并不是第一次扩容后对象,于是会因为第一次插入的值并不在旧的elementDate中,而将null值更新到新的数组中。这里我们举一个详细的例子:

现在有线程A和B分别要插入元素1和2,当线程A调用add方法时size是0,于是会进行一次扩容,此时线程B调用add方法时size仍然是0,所以也会进行扩容,假设此时线程A比线程B扩容先完成,此时list的elementDate是新的数组对象(由线程A构建),然后开始执行elementDate[size++] = 1的程序,这个过程中线程B扩容拿到的数组仍然是旧的elementDate,于是线程B构造一个新的数组(数据全部为null),然后使list的elementDate指向线程B构造的对象,那么线程A之前构造的elementDate也就被丢掉了,但是由于size已经自增,所以线程B会在索引为1的位置赋予2,那么此时数组元素就成了[null,2],当然如果线程B扩容比线程A先完成那么就可能为[null,1]。

大家如果在初始化的时候就已经开辟好足够大的容量,那么就不会出现上面的问题,关于上面的解释大家可以作为参考,因为不同的编译器可能javap得到的字节码可能会不同吧(这里我编译结果是size被复制两份,然后使用其中的一份加一更新到size中,然后用复制的另一份作为索引更新数组,但是网上得到信息大家都认为是数组先赋值,然后size自增)。

除了上面元素为null的情况外,还会有其他错误

  • 数量错误,集合数据正确
9[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
e7aaf5b734cc245091c075dc842c2190.png
9[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

大家是不是第一反应是不是觉得这种结果是由ArrayList本身的不安全特效造成的呢?实际上这种结果和ArrayList本身没有关系,只是因为我们打印不具有原子性所造成的。因为我们启用了多线程,主线程调用size方法时,可能多线程内部对list还在继续执行增加元素的操作,当主线程调用toString方法时,多线程已经执行完毕,所以元素数量正确,当然也有可能你调用toString方法时,多线程仍然未执行完,此时size和toString结果都不正确,如下:

8[1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 覆盖,这种情况的原因在上面的分析中以及提到,因为size++并不是原子性的,所以可能线程A自增的时候,线程B也进行一次自增,但是两次自增的结果是一样的,所以先完成的线程更新的数据会被后完成的线程覆盖掉
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值