原创作品转载请附: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浅谈并发容器的更多相关文章

  1. 浅谈Linux容器和镜像签名

    导读 从根本上说,几乎所有的主要软件,即使是开源软件,都是在基于镜像的容器技术出现之前设计的.这意味着把软件放到容器中相当于是一次平台移植.这也意味着一些程序可以很容易就迁移,而另一些就更困难. 我大 ...

  2. PHP解耦的三重境界(浅谈服务容器)

    阅读本文之前你需要掌握:PHP语法,面向对象 在完成整个软件项目开发的过程中,有时需要多人合作,有时也可以自己独立完成,不管是哪一种,随着代码量上升,写着写着就"失控"了,渐渐&q ...

  3. 浅谈并发和tomcat线程数

    假设Tomcat每到固定一个时间会有一个高峰,峰值的并发达到了3000以上,最后的结果是Tomcat线程池满了,日志看很多请求超过了1s. 服务器性能很好,Tomcat版本是7.0.54,配置如下 & ...

  4. 结合源码浅谈Spring容器与其子容器Spring MVC 冲突问题

    容器是整个Spring 框架的核心思想,用来管理Bean的整个生命周期. 一个项目中引入Spring和SpringMVC这两个框架,Spring是父容器,SpringMVC是其子容器,子容器可以看见父 ...

  5. JDK1.5新特性,基础类库篇,浅谈并发工具包(Concurrency Utilities)

    java.util.concurrent, java.util.concurrent.atomic, 和 java.util.concurrent.locks 包提供了高性能的.可扩展的框架,保证开发 ...

  6. 一只简单的网络爬虫(基于linux C/C++)————浅谈并发(IO复用)模型

    Linux常用的并发模型 Linux 下设计并发网络程序,有典型的 Apache 模型( Process Per Connection ,简称 PPC ), TPC ( Thread Per Conn ...

  7. Python | 浅谈并发锁与死锁问题

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Python专题的第24篇文章,我们一起来聊聊多线程场景当中不可或缺的另外一个部分--锁. 如果你学过操作系统,那么对于锁应该不陌生. ...

  8. 浅谈surging服务引擎中的rabbitmq组件和容器化部署

    1.前言 上个星期完成了surging 的0.9.0.1 更新工作,此版本通过nuget下载引擎组件,下载后,无需通过代码build集成,引擎会通过Sidecar模式自动扫描装配异构组件来构建服务引擎 ...

  9. 浅谈ArrayList

    浅谈ArrayList 废话不多说(事实是不会说),让我们直接进入正题 首先讲一讲最基本的ArrayList的初始化,也就是我们常说的构造函数,ArrayList给我们提供了三种构造方式,我们逐个来查 ...

随机推荐

  1. 初探内核之《Linux内核设计与实现》笔记下

    定时器和时间管理 系统中有很多与时间相关的程序(比如定期执行的任务,某一时间执行的任务,推迟一段时间执行的任务),因此,时间的管理对于linux来说非常重要. 主要内容: 系统时间 定时器 定时器相关 ...

  2. [洛谷] 通往奥格瑞玛的道路 [Vijos]

    题目背景 在艾泽拉斯大陆上有一位名叫歪嘴哦的神奇术士,他是部落的中坚力量 有一天他醒来后发现自己居然到了联盟的主城暴风城 在被众多联盟的士兵攻击后,他决定逃回自己的家乡奥格瑞玛 题目描述 在艾泽拉斯, ...

  3. TensorFlow2.0(7):激活函数

    .caret, .dropup > .btn > .caret { border-top-color: #000 !important; } .label { border: 1px so ...

  4. Hydra爆破神器使用

    参数详解: -R 根据上一次进度继续破解-S 使用SSL协议连接-s 指定端口-l 指定用户名-L 指定用户名字典(文件)-p 指定密码破解-P 指定密码字典(文件)-e 空密码探测和指定用户密码探测 ...

  5. Powershell基础之脚本执行

    Bat 这就是我们常用的Bat脚本,全名为批处理文件,脚本中就是我们在CMD中使用到的命令,这里提一个小问题:CMD的命令行执行命令的优先级是.bat > .exe,那么假如我放一个cmd.ba ...

  6. Halcon C# 联合编程问题(三)

    因为之前遇到的那个halcon处理的图片要转换成ImageSource的问题,迟迟没有找到好的解决方案, 于是决定直接在wpf中使用halcon提供的HWindowControlWPF,用于显示图片. ...

  7. Flink 从 0 到 1 学习 —— 如何自定义 Data Source ?

    前言 在 <从0到1学习Flink>-- Data Source 介绍 文章中,我给大家介绍了 Flink Data Source 以及简短的介绍了一下自定义 Data Source,这篇 ...

  8. Java Web 学习(1) —— Servlet

    Java Web 学习(1) —— Servlet 一. 什么是 Servlet Java Servlet 技术是Java体系中用于开发 Web 应用的底层技术. Servlet 是运行在 Servl ...

  9. CheckBox状态多选

    前: <StackPanel Margin="> <Label FontWeight="Bold">Application Options< ...

  10. 【MongoDB详细使用教程】五、MongoDB的数据库管理

    目录 1.数据库安全 1.1.创建管理员账号和密码 1.2.设置服务状态为需要验证用户 1.3.创建用户账户和密码 1.4.忘记密码/修改密码 2.主从服务器 2.1.创建服务器目录,用于分别存放主从 ...