ArrayList并发写出现Null值
ArrayList并非线程安全的容器,这一点大家可能都非常清楚,但是在并发写入的情况下,不安全的情况具体有哪些,大家是否很清楚呢?本篇文章重点聊一下出现null的情况,然后对于其他并发写的安全做一个简单的叙述
我们看下面的代码,打印List的元素数量以及打印存储的元素
List<Integer> 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]
大家是不是第一反应是不是觉得这种结果是由ArrayList本身的不安全特效造成的呢?实际上这种结果和ArrayList本身没有关系,只是因为我们打印不具有原子性所造成的。因为我们启用了多线程,主线程调用size方法时,可能多线程内部对list还在继续执行增加元素的操作,当主线程调用toString方法时,多线程已经执行完毕,所以元素数量正确,当然也有可能你调用toString方法时,多线程仍然未执行完,此时size和toString结果都不正确,如下:
8
[1, 2, 3, 4, 5, 6, 7, 8, 9]
- 覆盖,这种情况的原因在上面的分析中以及提到,因为size++并不是原子性的,所以可能线程A自增的时候,线程B也进行一次自增,但是两次自增的结果是一样的,所以先完成的线程更新的数据会被后完成的线程覆盖掉
- 讲真,你该做备份的有效性校验了
- memcache安装方法
- 设计模式专题(五)——工厂方法模式
- ASP.NET AJAX(11)__ScriptManagerUpdatePanel的支持成员功能控制成员脚本控件支持成员ScriptMode和ScriptPathLoadScriptsBeforeU
- SQL Server 2016新特性:动态数据屏蔽(DDM)
- ASP.NET AJAX(12)__浏览器兼容功能判断浏览器的类型和版本Sys.Browser针对DOM元素的兼容操作针对DOM事件的兼容操作
- 设计模式专题(六)——原型模式
- ASP.NET AJAX(13)__利用Microsoft AJAX Library开发客户端组件Sys.Component成员Sys.IDisposable成员Sys.INotifyDisposin
- 设计模式专题(七)——建造者模式
- ASP.NET AJAX(14)__UpdatePanel与服务器端脚本控件脚本控件的作用脚本控件的指责Extender模型脚本控件和Extender模型在PostBack中保持状态在UpdatePa
- ASP.NET AJAX(15)__构建高性能ASP.NET AJAX应用UpdatePanel的性能问题使用UpdatePanel的注意事项脚本加载避免脚本阻塞页面显示AjaxControlTool
- LINQ to SQL(1):基础入门
- 设计模式专题(十)——观察者模式
- LINQ to SQL(2):生成对象模型
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 经典排序算法-快速排序
- Stata | 解决 graph 中 x 轴刻度重叠问题
- Docker 垃圾回收机制补充
- 5分钟学会经典排序算法-归并排序
- Python | 爬取农业农村部政策法规并绘制词云图
- 5分钟学会经典排序算法-希尔排序
- Stata | 爬取企业景气指数与企业家信心指数
- Stata | 发出提示音的几种方式
- docker垃圾回收机制
- 让运维更简单的7种定时任务实现方式
- Notes | Chrome 浏览器常用快捷键
- Python | 从 PDF 中提取文本内容
- Stata | 自动生成中南财大2019拟录取硕士研究生分析报告
- Stata | 聊聊数据排序的几种方式
- 在生产中应用广泛的排序算法