浅谈Java中ArrayList线程不安全怎么办
作者:诺狗w 发布时间:2023-10-02 19:24:56
ArrayList线程不安全怎么办?
有三种解决方法:
使用对应的 Vector 类,这个类中的所有方法都加上了 synchronized 关键字
就和 HashMap 和 HashTable 的关系一样
使用 Collections 提供的 synchronizedList 方法,将一个原本线程不安全的集合类转换为线程安全的,使用方法如下:
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
其实 HashMap 也可以用这招:
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
这个看上去有点东西,其实也是给每个方法加上一个 synchronized,不过不是直接加在方法上,而是加在方法内部,只有当线程获取到 mutex 这个对象的锁,才能进入代码块:
public E get(int index) {
synchronized (mutex) {
return list.get(index);
}
}
使用 JUC 包下提供的 CopyOnWriteArrayList 类
其实 ConcurrentHashMap 也是 JUC 包下的
这里具体讨论一下 CopyOnWriteArrayList 这个类,它采用了“写时复制”的技术,也就是说,每当要往这个 list 中添加元素时,并不是直接就添加了,而是会先复制一份 list,然后在这个复制中添加元素,最后再修改指针的指向,看看 add 的源码:
public boolean add(E e) {
synchronized (lock) {
//得到当前的数组
Object[] es = getArray();
int len = es.length;
//复制一份并扩容
es = Arrays.copyOf(es, len + 1);
//把新元素添加进去
es[len] = e;
//修改指针的指向
setArray(es);
return true;
}
}
有人可能会疑惑,这有什么意义,这不也加了 synchronized 吗,而且还要复制数组,这**不是比 Vector 还要烂吗?
确实是这样的,在写操作比较多的场景下,CopyOnWriteArrayList 确实比 Vector 还要慢,但它有两个优势:
虽然写操作烂了,但读操作快了很多,因为在 vector 中,读操作也是需要锁的,而在这里,读操作就不需要锁了,get 方法比较短可能不便于理解,我们看看 indexOf 这个方法:
public int indexOf(Object o) {
Object[] es = getArray();
return indexOfRange(o, es, 0, es.length);
}
private static int indexOfRange(Object o, Object[] es, int from, int to) {
if (o == null) {
for (int i = from; i < to; i++)
if (es[i] == null)
return i;
} else {
//****here****
for (int i = from; i < to; i++)
if (o.equals(es[i]))
return i;
}
return -1;
}
可以发现,这个方法先把当前数组 array 交给了 es 这个变量,后续的所有操作都是基于 es 进行的(此时 array 和 es 都指向内存中的同一份数组 a1)
由于所有写操作都是在 a1 的拷贝上进行的(我们把内存中的这份拷贝称为 a2),因此不会影响到那些正在 a1 上进行的读操作,并且就算写操作执行完毕了,array 指向了 a2,也不会影响到 es 这个数组,因为 es 指向的还是 a1
试想,如果 vector 的读操作不加锁会出现什么情况?由于 vector 中所有的读写操作都是基于同一个数组的,因此虽然读操作一开始拿到的数组是没问题的,但在后续遍历的过程中(比如上面代码标注了 here 的地方),很可能出现其他线程对数组进行了修改,夸张点说,如果有个线程把数组给清空了,那么读操作就肯定会报错了,而对于 CopyOnWriteArrayList 来说,就算有清空的操作,那也是在 a2 上进行的,而读操作还是在 a1 上进行,不会有任何影响
在 forEach 遍历一个 vector 时,是不允许对 vector 进行修改的,会报出 ConcurrentModificationException 这个异常,理由很简单,因为只有一份数组,要是遍历到一半有其它线程把数组清空了不就出问题了吗,因此 java 干脆就直接禁止这种遍历时修改数组的行为了,但对于 CopyOnWriteArrayList 来说,它的遍历是一直在 a1 上进行的,其它写线程只能修改到 a2,这对 a1 是没有任何影响的,我们看一段代码来验证一下:
public class Test {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add(i);
}
//遍历时把数组清空
for (Integer i : list) {
System.out.println(i);
list.clear();
}
}
}
结果是没有报错,并且完整输出了 0~999 所有的数字,可见这里遍历的就是最开始的那个数组 a1,期间哪怕有再多的写操作也不会影响到 a1,因为所有的写操作都是在 a2 a3 a4 上进行的
综上所述,CopyOnWriteArrayList 的优点有两个:
读操作不需要锁,因此读读可以并发,读写也能并发,性能较好
forEach 遍历时也不需要锁(其实遍历也算是一种读操作吧),主要是遍历时数组可以被修改,不会报错(因为遍历的是 a1,改的是 a2 a3,对 a1 不会有影响)
但它的缺点也很明显,主要有两点:
首先,写操作的内存消耗非常大,每次修改数组都会进行一次拷贝,如果数组比较大或者修改次数比较多,很快就会消耗掉大量内存,触发 GC,因此在写多的场景下一定要慎用这个类
其次,所有读操作和 forEach 遍历都是基于旧数组 a1 的,就算遍历途中新增了一个很重要的数据,这个数据也是在 a2 中,遍历 a1 是无法得到这个数据的,总之就是,所有的读操作一旦开始,就无法再感知到最新的那些数据
可以发现一个有趣的事情,就是成也旧数组,败也旧数组,正因为所有读取都是基于旧数组 a1 的,因此可以不加锁就大胆进行,不怕有线程把数组改了,因为改动都是在 a2 a3 上的,跟 a1 没有关系,但也正因为所有读取都是基于旧数组 a1 的,因此一旦读取操作开始,就算有线程在数组中加入了一个很重要的数据,这个读取操作也是感知不到这个最新的数据的,因为这个最新的数据只会在 a2 中有
来源:https://www.cnblogs.com/ngxxx/p/15203821.html
猜你喜欢
- 示例1:public static String hello() { String s = "商务&qu
- 引言C#应用通过 Microsoft.Toolkit.Uwp.Notifications NuGet包可以很方便的发送本地通知(Window
- 使用方法 首先在Github或者Gitee上面新建一个仓库复制仓库的链接用idea在本地新建一个demo项目点击菜单栏的VCS,按
- 查了网上的资料,有比较全面的,但有一个问题就是容易出现一个文字和框子不符合的现象。(仔细看,蓝色字母和背景的灰色有空白)要消除这个空白,很简
- 前言 短时间提升自己最快的手段就是背面试题,最近总结了Java常用的面试题,分享给大家,希望大家都能圆梦大厂,加油,我命由我不由天
- 这篇文章主要介绍了Java如何把数组转换为ArrayList,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需
- java 弹幕小游戏的最初版本,供大家参考,具体内容如下最近在学习javaSE,根据b站视频老师的讲解,也参考了他的代码,做了一个弹幕小游戏
- 返回Json实体类属性大小写问题总归上述问题Rt,其实今天开发刚遇到,当时找了半天为啥前台传参后台却接收不到,原来是返回的时候返回小写,但是
- 缓存淘汰算法在高并发、高性能的质量要求不断提高时,我们首先会想到的就是利用缓存予以应对。第一次请求时把计算好的结果存放在缓存中,下次遇到同样
- 方式一: 配置文件 application.propertiesserver.port=7788方式二: java启动命令# 以应用参数的方
- 一、运行class文件执行带main方法的class文件,命令行为:java <CLASS文件名>注意:CLASS文件名不要带文
- 目前有两种流行Spring定时器配置:Java的Timer类和OpenSymphony的Quartz。1.Java Timer定时首先继承j
- 本文介绍了JAVA中实现原生的 socket 通信机制原理,分享给大家,具体如下:当前环境jdk == 1.8知识点socket 的连接处理
- 废话不多说,直接上代码,小伙伴们仔细看 * 释吧。/*简单的复制 剪切 粘贴 功能 操作: &nb
- 一、Monkey 是什么?Monkey 就是SDK中附带的一个工具。二、Monkey 测试的目的?:该工具用于进行压力测试。 然后开发人员结
- 1.算法效率算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。时间复杂度主要衡
- 标准c++中string类函数介绍注意不是CString之所以抛弃char*的字符串而选用C++标准程序库中的string类,是因为他和前者
- 网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。java.net包中J2SE的API包含有类和接口,它们提供低层
- 本文实例为大家分享了android自定义环形对比图的具体代码,供大家参考,具体内容如下1.首先在res/values里创建一个attr.xm
- Java jar打包成exe应用程序,可在无JDK/JRE环境下运行老师让做一个小项目,但是需要打包发布出来,因此在网上查了很多资料之后总结