情况③:
10个线程 显然还是 太少了, 而且我电脑机子又好, 终于出现 ‘不安全’情况了 ,非常难得。
多线程操作ArrayList 导致出现 add赋值 出现 null 情景分析 :
为什么会出现,先看看源码 ,
Object[] elementData : 保存所有元素值的 数组
size : elementData中存储的元素个数
再看看 add 函数的 源码 :
ensureExplicitCapacity ()函数:
将当前的新元素加到列表后面,判断列表的 elementData 数组的大小是否满足。
如果 size + 1 的这个需求长度大于 elementData 这个数组的长度,那么就要对这个数组进行扩容。
elementData[size++] = e :
e是传入的 值, 把这个值 赋值在 elementData数组的 size++ 位置 。
大家看出来问题没?
这两步没有和在一块操作。
也就说如果出现这个扩容的触发 和后面 赋值 并发情况 ,那么就有好戏看了。
ArrayList是基于数组实现,数组大小一旦确定就无法更改。
ArrayList的扩容: 将旧数组容器的元素拷贝到新大小的数组中(Arrays.copyOf函数)。
而 通过new ArrayList()实例的对象初始化的大小是0,所以第一次插入就肯定会触发扩容。
这里又必须给大家推荐一篇好文章了:
(没错也是我写的,但是看到这,你别去看这篇,跟着我现在的思路继续分析 这个null值出现的情景,实在很感兴趣,自己一会再看)
Java ArrayList new出来,默认的容量到底是0还是10 ?
看看我们的截图,第一个数据是 null 。
有趣。
第一个数据是 null (其实应该称为 执行扩容操作,并发导致出现null值 )分析 :
第一个线程A 插入数据时 属于首次add ,发现需要扩容, ok , 线程A 去扩容去了。
然后 我们是多线程操作场景, for循环第二次,触发new第二个线程B来了,线程B去add的时候,
因为线程A第一次扩容可能并没完成,所以导致 线程B 扩容所拿到list的elementDate是旧的,并不是线程A第一次扩容后对象,线程B拿到的size还是 0 ,所以线程B 也认为自己是第一次add ,也需要扩容。
幻想一下 A 、B 线程的并发 一起进入扩容场景:
那么线程A 是第一次add的时候,他知道他要去扩容, 他自己 扩容 完,自己整了个list的新elementDate ,然后 就开始赋值 elementDate[size++] = A的UUID值。
在线程A这个操作的过程中,线程B 在做什么?
线程B一开始 不巧也是以为要扩容,他拿着一个旧的 list的elementDate 也整了一个新的数组 ,
然后把 整个 list的 elementDate 引用指向 B线程自己弄出来的对象
this.elementData = B新构建的对象(这对象全部值为null);
然后做什么?
然后 线程B 开始执行 elementDate[size++] = B的UUID值。
这里的好玩点是什么?
线程A 的值 赋值在 他创建出来的 elementDate 里面,然后触发 size++ 。
但是线程B 呢, 把 this.elementData 指向了自己的新弄出来的, 所以 A 的值 无情被抛弃, 但是 线程B 开始赋值的时候,
看看这个size在源码里的情况:
public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable{ transient Object[] elementData; //这是大家共用的 size private int size;}
size是大家共用的, size 被 线程A 加1了 ,所以就出现 线程B赋值的时候 执行 elementDate[size++] = B的UUID值,出来的结果是
[null , B的UUID值]
null 就是这么来的 ! 能看到这的人,友情提示,你已经阅读了3500字。当然还没完事。
情况④:
java.util.ConcurrentModificationException 并发冲突
直接定位报错函数:
这个其实 之前分析过:
modCount是修改记录数,expectedModCount是期望修改记录数;
初始化的时候 expectedModCount=modCount ;
ArrayList的add函数、remove函数 操作都有对modCount++操作,当expectedModCount和modCount值不相等, 那就会报并发错误了(其实这个不是仅仅是多线程的问题,是这个ArrayList 代码next函数的问题,更多细节可以有空看看 Java 移除List中的元素,这玩意讲究!)。
那么到这 我们大概知道 这个ArrayList的不安全 问题了, 说白了就是 2行代码没上锁操作。
最简单的方式, 也是面经上经常看到的 使用 Vector :
List resultList = new Vector();
看看vector怎么保证安全的:
其次 是 使用 Collections里面的synchronizedList :
List resultList =Collections.synchronizedList(new ArrayList());
看看synchronizedList 怎么保证安全的:
还有可以使用CopyOnWriteArrayList :
List resultList = new CopyOnWriteArrayList();
看看CopyOnWriteArrayList 怎么保证安全的:
ps:
CopyOnWriteArrayList 的set 也是上锁
但是get 没有, 也就是说,get可能在多线程场景使用,拿到的是旧数据是可能的(也就是当前能读到的list里面的数据)
那么就CopyOnWriteArrayList的 set\add\get 函数,你能预料到它的不好点么?
1.set add 都选择使用了Arrays.copyOf复制操作
所以存在 内存占用以及耗时问题,当数组元素越来越多的时候。
2. get 多线程过程读取数据不是实时,那就可能出现 数据不一致问题,但是最终数据是一致的(读多写少就很合适)。
好了,该篇就到这吧。