代码审查:从 ArrayList 说线程安全
本文从代码审查过程中发现的一个 ArrayList 相关的「线程安全」问题出发,来剖析和理解线程安全。
案例分析
前两天在代码 Review 的过程中,看到有小伙伴用了类似以下的写法:
List<String> resultList = new ArrayList<>();
paramList.parallelStream().forEach(v -> {
String value = doSomething(v);
resultList.add(value);
});
印象中 ArrayList 是线程不安全的,而这里会多线程改写同一个 ArrayList 对象,感觉这样的写法会有问题,于是看了下 ArrayList 的实现来确认问题,同时复习下相关知识。
先贴个概念:
线程安全 是程式设计中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 ——维基百科
我们来看下 ArrayList 源码里与本话题相关的关键信息:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// ...
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer...
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*/
private int size;
// ...
/**
* Appends the specified element to the end of this list...
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// ...
}
从中我们可以关注到关于 ArrayList 的几点信息:
- 使用数组存储数据,即
elementData
- 使用 int 成员变量
size
记录实际元素个数 add
方法逻辑与执行顺序:- 执行
ensureCapacityInternal(size + 1)
:确认elementData
的容量是否够用,不够用的话扩容一半(申请一个新的大数组,将elementData
里的原有内容 copy 过去,然后将新的大数组赋值给elementData
) - 执行
elementData[size] = e;
- 执行
size++
- 执行
为了方便理解这里讨论的「线程安全问题」,我们选一个最简单的执行路径来分析,假设有 A 和 B 两个线程同时调用 ArrayList.add
方法,而此时 elementData
容量为 8,size
为 7,足以容纳一个新增的元素,那么可能发生什么现象呢?
一种可能的执行顺序是:
- 线程 A 和 B 同时执行了
ensureCapacityInternal(size + 1)
,因7 + 1
并没超过elementData
的容量 8,所以并未扩容 - 线程 A 先执行
elementData[size++] = e;
,此时size
变为 8 - 线程 B 执行
elementData[size++] = e;
,因为elementData
数组长度为 8,却访问elementData[8]
,数组下标越界
程序会抛出异常,无法正常执行完,根据前文提到的线程安全的定义,很显然这已经是属于线程不安全的情况了。
构造示例代码验证
有了以上的理解之后,我们来写一段简单的示例代码,验证以上问题确实可能发生:
List<Integer> resultList = new ArrayList<>();
List<Integer> paramList = new ArrayList<>();
int length = 10000;
for (int i = 0; i < length; i++) {
paramList.add(i);
}
paramList.parallelStream().forEach(resultList::add);
执行以上代码有可能表现正常,但更可能是遇到以下异常:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:598)
at java.util.concurrent.ForkJoinTask.reportException(ForkJoinTask.java:677)
at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:735)
at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:160)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:174)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:583)
at concurrent.ConcurrentTest.main(ConcurrentTest.java:18)
Caused by: java.lang.ArrayIndexOutOfBoundsException: 1234
at java.util.ArrayList.add(ArrayList.java:465)
at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1384)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291)
at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1067)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1703)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:172)
从我这里试验的情况来看,length
值小的时候,因为达到容量边缘需要扩容的次数少,不易重现,将 length
值调到比较大时,异常抛出率就很高了。
实际上除了抛出这种异常外,以上场景还可能造成数据覆盖/丢失、ArrayList 里实际存放的元素个数与 size 值不符等其它问题,感兴趣的同学可以继续挖掘一下。
解决方案
对这类问题常见的有效解决思路就是对共享的资源访问加锁。
我提出代码审查的修改意见后,小伙伴将文首代码里的
List<String> resultList = new ArrayList<>();
修改为了
List<String> resultList = Collections.synchronizedList(new ArrayList<>());
这样实际最终会使用 SynchronizedRandomAccessList
,看它的实现类,其实里面也是加锁,它内部持有一个 List,用 synchronized 关键字控制对 List 的读写访问,这是一种思路——使用线程安全的集合类,对应的还可以使用 Vector 等其它类似的类来解决问题。
另外一种方思路是手动对关键代码段加锁,比如我们也可以将
resultList.add(value);
修改为
synchronized (mutex) {
resultList.add(value);
}
小结
Java 8 的并行流提供了很方便的并行处理、提升程序执行效率的写法,我们在编码的过程中,对用到多线程的地方要保持警惕,有意识地预防此类问题。
对应的,我们在做代码审查的过程中,也要对涉及到多线程使用的场景时刻绷着一根弦,在代码合入前把好关,将隐患拒之门外。
参考
代码审查:从 ArrayList 说线程安全的更多相关文章
- ArrayList实现线程安全的blogs
ArrayList是线程不安全的,轻量级的.如何使ArrayList线程安全? 1.继承Arraylist,然后重写或按需求编写自己的方法,这些方法要写成synchronized,在这些synchro ...
- ArrayList的线程安全测试
public class TestThread implements Runnable{ private List list; CountDownLatch cdl; public TestThrea ...
- Vector线程安全,ArrayList非线程安全
http://baijiahao.baidu.com/s?id=1638844080997170869&wfr=spider&for=pc Vector线程安全,ArrayList非线 ...
- ArrayList,Vector线程安全性测试
import java.util.ArrayList; import java.util.List; //实现Runnable接口的线程 public class HelloThread implem ...
- 为什么说ArrayList是线程不安全的?
一.概述 对于ArrayList,相信大家并不陌生.这个类是我们平时接触得最多的一个列表集合类. 面试时相信面试官首先就会问到关于它的知识.一个经常被问到的问题就是:ArrayList是否是线程安全的 ...
- 【面试专栏】ArrayList 非线程安全案例并提供三种解决方案
1. 复现问题 import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * 复现问题 * * @au ...
- ArrayList实现线程的几种方法
第一种.给方法名加synchronized Public synchronized void method(){ //-. } 第二种 New synchronized arraylist(); 第三 ...
- ArrayList如何保证线程安全
ArrayList是线程不安全的,轻量级的.如何使ArrayList线程安全? 1.继承Arraylist,然后重写或按需求编写自己的方法,这些方法要写成synchronized,在这些synchro ...
- 集合框架,ArrayList和Vector的区别,让arrayList线程安全的几种方案
boolean add(E e) 将指定的元素添加到此列表的尾部. void add(int index, E element) 将指定的元素插入此列表中的指定位置. boolean addAll(C ...
随机推荐
- 2019牛客多校第九场B Quadratic equation(二次剩余定理)题解
题意: 传送门 已知\(0 <= x <= y < p, p = 1e9 + 7\)且有 \((x+y) = b\mod p\) \((x\times y)=c\mod p\) 求解 ...
- docker07-数据存储
Docker 内部以及容器之间管理数据,在容器中管理数据主要有两种方式: 数据卷(Volumes) 挂载主机目录 (Bind mounts) 数据卷 是一个可供一个或多个容器使用的特殊目录,它绕过 U ...
- HTML教程(看完这篇就够了)
HTML教程 超文本标记语言(英语:HyperText Markup Language,简称:HTML)是一种用于创建网页的标准标记语言.您可以使用 HTML 来建立自己的 WEB 站点,HTML 运 ...
- TypeScript tuple 元组
TypeScript tuple 元组 元组类型允许您用固定数量的元素表示数组,这些元素的类型是已知的,但不必相同. "use strict"; /** * * @author x ...
- 图解 git workflow
图解 git workflow 图解 git 工作流 git-flow https://www.git-tower.com/learn/git/ebook/cn/command-line/advanc ...
- skills share & free videos
skills share & free videos 技术分享 & 免费视频 https://www.infoq.cn/video/list WebAssembly https://w ...
- 如何导出android内部存储的文件(不用root)
这段时间公司项目,涉及到数据缓存,由于需要缓冲的数据太多.太大,通过网络请求,再缓存到本地sqlite数据库,太费时间,消耗流量.所以准备先在本地保存一个标准版sqlite数据库(包含数据),打包到a ...
- Objec.assign & bug
Objec.assign & bug shallow copy https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Referenc ...
- 使用hive增量更新
目录 1.增量更新 2.对第一种情况 2.1.准备工作 2.2.更新数据 3.对第二种情况 3.1.准备工作 3.2.方法1 3.3.方法2 参考文末文章,加上自己的理解. 1.增量更新 有一个 ba ...
- 1100 Mars Numbers——PAT甲级真题
1100 Mars Numbers People on Mars count their numbers with base 13: Zero on Earth is called "tre ...