由浅入深——从ArrayList浅谈并发容器
原创作品转载请附:https://www.cnblogs.com/superlsj/p/11655523.html
一、一个案例引发的思考
public class ArrayListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}).start();
}
}
}
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
at java.util.AbstractCollection.toString(Unknown Source)
at java.lang.String.valueOf(Unknown Source)
at java.io.PrintStream.println(Unknown Source)
at com.qlu.test1.ArrayListTest.lambda$0(ArrayListTest.java:13)
at java.lang.Thread.run(Unknown Source)
即所谓的并发修改异常。我们先来分析一下为什么会报这个错。
二、错误产生的原因
我们知道,ArrayList是线程不安全的,它的所有方法没有加Synchronized锁:例如
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
也就是说,上面定义的50个线程都会抢占此ArrayList。那么为什么会爆出错误呢?当我把System.out.println(list);删除后,错误就没报了。那么问题可能出在ArrayList的toString()方法。查看源码会发现
ArrayList类并没有toString()方法。这个toString方法是从ArrayList的父类的父类:AbstractCollection类继承而来的,toString()方法源码如下:
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
toString()方法遍历集合所有的元素拼接了一个字符串返回。那么问题出在哪呢?首先记住两个变量:modCount和expectedModCount。
由上面的add方法的源码可以看到,在方法体内的先执行了ensureCapacityInternal(size + 1);方法,这个方法中调用了ensureExplicitCapacity()方法,而在此方法的第一行:赫然写着modCount++
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
也就是说集合的每一次添加操作都会触发modCount++操作,而并没有expectedModCount++,在来看一下expectedModCount是何时被赋值的:
在集合完成创建后,expectedModCount的值是0,在创建迭代器时将modCount的值赋给了expectedModCount:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
......
而在迭代器创建以后,expectedModCount的值就不再改变。也就是说此后的add操作会改变modCount,而不会改变expectedModCount。那么重点来了。在使用迭代器的next()方法时,会调用checkForComodification()方法验证expectedModCount和modCount是否相等:
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
checkForComodification()方法源码如下:如果 modCount != expectedModCount 不相等,就抛出并发修改异常。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
在回到开头的案例:
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}).start();
}
由于list是共享资源,即所有线程共享一个modCount资源。假设A线程添加一个UUID后,由于需要输出list,而输出list需要创建迭代器,此时他根据集合的modCount假设为2,那么expectedModCount也是2,但是A线程next()迭代集合拼接字符串的操作未完成,CPU就将资源转给了其他线程,假设转给了线程B,B拿到资源后由于进行了add操作,所以list的modCount++;假设modCount++后为3,虽然B线程创建迭代器时会根据最新的modCount给expectedModCount=3,但是如果此时CPU又将资源转给了线程A,线程A加载自己原先的上下文,或得上一次执行时的迭代器对象,而此迭代器对象持有的 expectedModCount为2,而共享资源里的modCount却被B线程更新到了3,此时如果A线程继续迭代next(),就会发现modCount != expectedModCount 不相等,就抛出并发修改异常。
三、如何解决问题
1、将ArrayList改成Vector,不再赘述。
2、使用集合工具类Collections
public class ArrayListTest{
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}).start();
}
}
}
此方法同样可以用于其他线程不安全的集合类,例如:set、map
3、使用并发容器【推荐使用】
public class ArrayListTest{
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}).start();
}
}
}
将ArrayList换成CopyOnWriteArrayList问题就解决了,CopyOnWriteArrayList是怎么解决问题的呢?这就要提到一个重要的技术:写时复制技术。
四、写时复制技术
ArrayList和CopyOnWriteArrayList,都是集合,都是通过add增加元素,那么区别到底在哪里呢?先来看看CopyOnWriteArrayList的add方法的源码。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
与Vector不同的是:CopyOnWriteArrayList并没有采用传统的synchronized,而是采用了ReentrantLock可重入锁。关于锁的只是本章节不做讨论,这里主要研究写时复制技术是如何实现的。add方法先创建了一个Object类型的数组,指向了旧数组,然后定义变量len保存数组容量,由于此数组从length从0开始,每增加一个元素,扩容一单位,所以size=length。然后定义新数组newElements,长度为len+1,在给新数组的新增位置添加add方法传进来的新元素后,调用setArray方法将旧数组的引用指向这个新数组。
整个过程就像墙上贴着登记信息,传统的ArrayList的解决方法是:谁抢到登记表谁填信息,如果谁抢到后还没写完就被别人抢去了,那旧出现了数据不安全的问题。CopyOnWriteArrayList的解决方案就像是,第一个登记的将墙上的表格赋值一份到自己的线程私有的空间,等自己写完后就贴回墙上覆盖原来的表,内存是消耗了一点,但是绝对安全。在这个过程中还涉及了一个版本号的问题,假设此时两名名同学同时将墙上的空表(假设版本为1.0)复制一份自己填写姓名,但是张三明显会比诸葛孔明写得快,于是率先将自己的表贴到墙上覆盖了原表,并将表格版本提升到了2.0,而此时诸葛孔明写完了准备提交,JVM会校验版本信息,发现诸葛孔明不是基于最新版本的数据做的修改,所以修改无效,此时诸葛孔明同学就需要重新复制一份填写姓名。
这样的思想在软件设计时非常常见,Git在版本控制上也使用了这样的乐观锁技术。
附:新兵蛋子,如有错误,还请各位大哥指正。
由浅入深——从ArrayList浅谈并发容器的更多相关文章
- 浅谈Linux容器和镜像签名
导读 从根本上说,几乎所有的主要软件,即使是开源软件,都是在基于镜像的容器技术出现之前设计的.这意味着把软件放到容器中相当于是一次平台移植.这也意味着一些程序可以很容易就迁移,而另一些就更困难. 我大 ...
- PHP解耦的三重境界(浅谈服务容器)
阅读本文之前你需要掌握:PHP语法,面向对象 在完成整个软件项目开发的过程中,有时需要多人合作,有时也可以自己独立完成,不管是哪一种,随着代码量上升,写着写着就"失控"了,渐渐&q ...
- 浅谈并发和tomcat线程数
假设Tomcat每到固定一个时间会有一个高峰,峰值的并发达到了3000以上,最后的结果是Tomcat线程池满了,日志看很多请求超过了1s. 服务器性能很好,Tomcat版本是7.0.54,配置如下 & ...
- 结合源码浅谈Spring容器与其子容器Spring MVC 冲突问题
容器是整个Spring 框架的核心思想,用来管理Bean的整个生命周期. 一个项目中引入Spring和SpringMVC这两个框架,Spring是父容器,SpringMVC是其子容器,子容器可以看见父 ...
- JDK1.5新特性,基础类库篇,浅谈并发工具包(Concurrency Utilities)
java.util.concurrent, java.util.concurrent.atomic, 和 java.util.concurrent.locks 包提供了高性能的.可扩展的框架,保证开发 ...
- 一只简单的网络爬虫(基于linux C/C++)————浅谈并发(IO复用)模型
Linux常用的并发模型 Linux 下设计并发网络程序,有典型的 Apache 模型( Process Per Connection ,简称 PPC ), TPC ( Thread Per Conn ...
- Python | 浅谈并发锁与死锁问题
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Python专题的第24篇文章,我们一起来聊聊多线程场景当中不可或缺的另外一个部分--锁. 如果你学过操作系统,那么对于锁应该不陌生. ...
- 浅谈surging服务引擎中的rabbitmq组件和容器化部署
1.前言 上个星期完成了surging 的0.9.0.1 更新工作,此版本通过nuget下载引擎组件,下载后,无需通过代码build集成,引擎会通过Sidecar模式自动扫描装配异构组件来构建服务引擎 ...
- 浅谈ArrayList
浅谈ArrayList 废话不多说(事实是不会说),让我们直接进入正题 首先讲一讲最基本的ArrayList的初始化,也就是我们常说的构造函数,ArrayList给我们提供了三种构造方式,我们逐个来查 ...
随机推荐
- python selenium鼠标滑动操作
先安装pyautogui: pip install pyautogui #coding=utf-8 import pyautogui from selenium import webdriver fr ...
- HTML5开发常见的7个框架,你知道几个?
互联网的迅速发展,软件行业成了更多年轻人的就业选择.HTML5简单易学门槛低,是Web时代前端开发超好用的工具.而HTML5开发人员的就业薪资也远远高于其他行业. 资料显示,初级HTML5开发人员的平 ...
- opencv实践::对象的提取
问题描述 真实案例,对图像中对象进行提取,获取这样对象,去掉其它干扰和非目标对象. 解决思路 二值分割 + 形态学处理 +横纵比计算 #include <opencv2/opencv.hpp&g ...
- 自然语言处理(NLP)
苹果语音助手Siri的工作流程: 听 懂 思考 组织语言 回答 这其中每一步骤涉及的流程为: 语音识别 自然语言处理 - 语义分析 逻辑分析 - 结合业务场景与上下文 自然语言处理 - 分析结果生成自 ...
- C#刷遍Leetcode面试题系列连载(5):No.593 - 有效的正方形
上一篇 LeetCode 面试题中,我们分析了一道难度为 Easy 的数学题 - 自除数,提供了两种方法.今天我们来分析一道难度为 Medium 的面试题. 今天要给大家分析的面试题是 LeetCod ...
- 压敏电阻的保护作用—NDF达孚电子科技
压敏电阻是常见的电子元器件之一,它的保护作用被大家熟知和运用.压敏电阻的主要用于在电路承受过压时进行电压钳位,吸收多余的电流以保护灵敏器件.压敏电阻的导电特性随着施加电压的变化呈非线性变化,它能保护电 ...
- 如何利用Fiddler4进行Android APP / IOS APP抓包
Fiddler抓包 1.Fiddler介绍 Fiddler是一个http协议调试代理工具,它能够记录并检查所有你的电脑和互联网之间的http通讯,设置断点,查看所有的“进出”Fiddler的数据. F ...
- 树莓派apt报错:E: '\Release' 这个值对 APT::Default-Release 是无效的,因为在源里找不到这样的发行
E: '\jessie' 这个值对 APT::Default-Release 是无效的,因为在源里找不到这样的发行 开始尝试了各种方法, 换apt源, 改/etc/apt/apt.conf.d/10d ...
- nginx 前后端分离 代理转发,解决跨域问题
场景 适用于公司有前端,项目采用前后端分离.类似于我们 后端 springboot 提供接口,前端专门写html调用相应的接口,解决跨域问题 配置说明 worker_processes 1; even ...
- .Net Core3.0依赖注入DI
构建ASP.NET Core应用程序的时候,依赖注入已成为了.NET Core的核心,这篇文章,我们理一理依赖注入的使用方法. 不使用依赖注入 首先,我们创建一个ASP.NET Core Mvc项目, ...