CopyOnWriteArrayList(复制数组 去实现)
一、Vector和SynchronizedList
1.1回顾线程安全的Vector和SynchronizedList
我们知道ArrayList是用于替代Vector的,Vector是线程安全的容器。因为它几乎在每个方法声明处都加了synchronized关键字来使容器安全。
如果使用Collections.synchronizedList(new ArrayList())来使ArrayList变成是线程安全的话,也是几乎都是每个方法都加上synchronized关键字的,只不过它不是加在方法的声明处,而是方法的内部。
1.2Vector和SynchronizedList可能会出现的问题
在讲解CopyOnWrite容器之前,我们还是先来看一下线程安全容器的一些可能没有注意到的地方~
下面我们直接来看一下这段代码:
// 得到Vector最后一个元素
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
// 删除Vector最后一个元素
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
复制代码
以我们第一反应来分析一下上面两个方法:在多线程环境下,是否有问题?
- 我们可以知道的是Vector的
size()和get()以及remove()都被synchronized修饰的。
答案:从调用者的角度是有问题的
我们可以写段代码测试一下:
import java.util.Vector;
public class UnsafeVectorHelpers {
public static void main(String[] args) {
// 初始化Vector
Vector<String> vector = new Vector();
vector.add("关注公众号");
vector.add("Java3y");
vector.add("买Linux可到我下面的链接,享受最低价");
vector.add("给3y加鸡腿");
new Thread(() -> getLast(vector)).start();
new Thread(() -> deleteLast(vector)).start();
new Thread(() -> getLast(vector)).start();
new Thread(() -> deleteLast(vector)).start();
}
// 得到Vector最后一个元素
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
// 删除Vector最后一个元素
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
复制代码
可以发现的是,有可能会抛出异常的:
原因也很简单,我们照着流程走一下就好了:
- 线程A执行
getLast()方法,线程B执行deleteLast()方法 - 线程A执行
int lastIndex = list.size() - 1;得到lastIndex的值是3。同时,线程B执行int lastIndex = list.size() - 1;得到的lastIndex的值也是3 - 此时线程B先得到CPU执行权,执行
list.remove(lastIndex)将下标为3的元素删除了 - 接着线程A得到CPU执行权,执行
list.get(lastIndex);,发现已经没有下标为3的元素,抛出异常了.
出现这个问题的原因也很简单:
getLast()和deleteLast()这两个方法并不是原子性的,即使他们内部的每一步操作是原子性的(被Synchronize修饰就可以实现原子性),但是内部之间还是可以交替执行。- 这里的意思就是:
size()和get()以及remove()都是原子性的,但是如果并发执行getLast()和deleteLast(),方法里面的size()和get()以及remove()是可以交替执行的。
- 这里的意思就是:
要解决上面这种情况也很简单,因为我们都是对Vector进行操作的,只要操作Vector前把它锁住就没毛病了!
所以我们可以改成这样子:
// 得到Vector最后一个元素
public static Object getLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}
// 删除Vector最后一个元素
public static void deleteLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
复制代码
ps:如果有人去测试一下,发现会抛出异常java.lang.ArrayIndexOutOfBoundsException: -1,这是没有检查角标的异常,不是并发导致的问题。
经过上面的例子我们可以看看下面的代码:
public static void main(String[] args) {
// 初始化Vector
Vector<String> vector = new Vector();
vector.add("关注公众号");
vector.add("Java3y");
vector.add("买Linux可到我下面的链接,享受最低价");
vector.add("给3y加鸡腿");
// 遍历Vector
for (int i = 0; i < vector.size(); i++) {
// 比如在这执行vector.clear();
//new Thread(() -> vector.clear()).start();
System.out.println(vector.get(i));
}
}
复制代码
同样地:如果在遍历Vector的时候,有别的线程修改了Vector的长度,那还是会有问题!
- 线程A遍历Vector,执行
vector.size()时,发现Vector的长度为5 - 此时很有可能存在线程B对Vector进行
clear()操作 - 随后线程A执行
vector.get(i)时,抛出异常
在JDK5以后,Java推荐使用for-each(迭代器)来遍历我们的集合,好处就是简洁、数组索引的边界值只计算一次。
如果使用for-each(迭代器)来做上面的操作,会抛出ConcurrentModificationException异常
SynchronizedList在使用迭代器遍历的时候同样会有问题的,源码已经提醒我们要手动加锁了。
如果想要完美解决上面所讲的问题,我们可以在遍历前加锁:
// 遍历Vector
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
}
复制代码
有经验的同学就可以知道:哇,遍历一下容器都要我加上锁,这这这不是要慢死了吗.的确是挺慢的..
所以我们的CopyOnWriteArrayList就登场了!
二、CopyOnWriteArrayList(Set)介绍
一般来说,我们会认为:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品。
无论是Hashtable-->ConcurrentHashMap,还是说Vector-->CopyOnWriteArrayList。JUC下支持并发的容器与老一代的线程安全类相比,总结起来就是加锁粒度的问题
- Hashtable、Vector加锁的粒度大(直接在方法声明处使用synchronized)
- ConcurrentHashMap、CopyOnWriteArrayList加锁粒度小(用各种的方式来实现线程安全,比如我们知道的ConcurrentHashMap用了cas锁、volatile等方式来实现线程安全..)
- JUC下的线程安全容器在遍历的时候不会抛出ConcurrentModificationException异常
所以一般来说,我们都会使用JUC包下给我们提供的线程安全容器,而不是使用老一代的线程安全容器。
下面我们来看看CopyOnWriteArrayList是怎么实现的,为什么使用迭代器遍历的时候就不用额外加锁,也不会抛出ConcurrentModificationException异常。
2.1CopyOnWriteArrayList实现原理
我们还是先来回顾一下COW:
如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。
参考自维基百科:zh.wikipedia.org/wiki/%E5%AF…
之前写博客的时候,如果是要看源码,一般会翻译一下源码的注释并用图贴在文章上的。Emmm,发现阅读体验并不是很好,所以我这里就直接概括一下源码注释说了什么吧。另外,如果使用IDEA的话,可以下一个插件Translation(免费好用).
概括一下CopyOnWriteArrayList源码注释介绍了什么:
- CopyOnWriteArrayList是线程安全容器(相对于ArrayList),底层通过复制数组的方式来实现。
- CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,并且遍历的时候就不用额外加锁
- 元素可以为null
2.1.1看一下CopyOnWriteArrayList基本的结构
/** 可重入锁对象 */
final transient ReentrantLock lock = new ReentrantLock();
/** CopyOnWriteArrayList底层由数组实现,volatile修饰 */
private transient volatile Object[] array;
/**
* 得到数组
*/
final Object[] getArray() {
return array;
}
/**
* 设置数组
*/
final void setArray(Object[] a) {
array = a;
}
/**
* 初始化CopyOnWriteArrayList相当于初始化数组
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
复制代码
看起来挺简单的,CopyOnWriteArrayList底层就是数组,加锁就交由ReentrantLock来完成。
2.1.2常见方法的实现
根据上面的分析我们知道如果遍历Vector/SynchronizedList是需要自己手动加锁的。
CopyOnWriteArrayList使用迭代器遍历时不需要显示加锁,看看add()、clear()、remove()与get()方法的实现可能就有点眉目了。
首先我们可以看看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;
// 将volatile Object[] array 的指向替换成新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
复制代码
通过代码我们可以知道:在添加的时候就上锁,并复制一个新数组,增加操作在新数组上完成,将array指向到新数组中,最后解锁。
再来看看size()方法:
public int size() {
// 直接得到array数组的长度
return getArray().length;
}
复制代码
再来看看get()方法:
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
复制代码
那再来看看set()方法
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 得到原数组的旧值
Object[] elements = getArray();
E oldValue = get(elements, index);
// 判断新值和旧值是否相等
if (oldValue != element) {
// 复制新数组,新值在新数组中完成
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
// 将array引用指向新数组
setArray(newElements);
} else {
// Not quite a no-op; enssures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
复制代码
对于remove()、clear()跟set()和add()是类似的,这里我就不再贴出代码了。
总结:
- 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
- 写加锁,读不加锁
2.1.3剖析为什么遍历时不用调用者显式加锁
常用的方法实现我们已经基本了解了,但还是不知道为啥能够在容器遍历的时候对其进行修改而不抛出异常。所以,来看一下他的迭代器吧:
// 1. 返回的迭代器是COWIterator
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
// 2. 迭代器的成员属性
private final Object[] snapshot;
private int cursor;
// 3. 迭代器的构造方法
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 4. 迭代器的方法...
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
//.... 可以发现的是,迭代器所有的操作都基于snapshot数组,而snapshot是传递进来的array数组
复制代码
到这里,我们应该就可以想明白了!CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组!
2.1.4CopyOnWriteArrayList缺点
看了上面的实现源码,我们应该也大概能分析出CopyOnWriteArrayList的缺点了。
- 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行
add()、set()、remove()的话,那是比较耗费内存的。- 因为我们知道每次
add()、set()、remove()这些增删改操作都要复制一个数组出来。
- 因为我们知道每次
- 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
- 从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用
setArray()了)。但是线程A迭代出来的是原有的数据。
- 从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用
2.1.5CopyOnWriteSet
CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
作者:Java3y
链接:https://juejin.im/post/5be23e6ef265da6135720d61
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
CopyOnWriteArrayList(复制数组 去实现)的更多相关文章
- CopyOnWriteArrayList你都不知道,怎么拿offer?
前言 只有光头才能变强 前一阵子写过一篇COW(Copy On Write)文章,结果阅读量很低啊...COW奶牛!Copy On Write机制了解一下 可能大家对这个技术比较陌生吧,但这项技术是挺 ...
- 【JUC】JDK1.8源码分析之CopyOnWriteArrayList(六)
一.前言 由于Deque与Queue有很大的相似性,Deque为双端队列,队列头部和尾部都可以进行入队列和出队列的操作,所以不再介绍Deque,感兴趣的读者可以自行阅读源码,相信偶了Queue源码的分 ...
- 图解集合3:CopyOnWriteArrayList
初识CopyOnWriteArrayList 第一次见到CopyOnWriteArrayList,是在研究JDBC的时候,每一个数据库的Driver都是维护在一个CopyOnWriteArrayLis ...
- 如何线程安全地遍历List:Vector、CopyOnWriteArrayList
遍历List的多种方式 在讲如何线程安全地遍历List之前,先看看通常我们遍历一个List会采用哪些方式. 方式一: for(int i = 0; i < list.size(); i++) { ...
- Java多线程系列--“JUC集合”02之 CopyOnWriteArrayList
概要 本章是"JUC系列"的CopyOnWriteArrayList篇.接下来,会先对CopyOnWriteArrayList进行基本介绍,然后再说明它的原理,接着通过代码去分析, ...
- 读CopyOnWriteArrayList有感
除了加锁外,其实还有一种方式可以防止并发修改异常,这就是将读写分离技术(不是数据库上的). 先回顾一下一个常识: 1.JAVA中“=”操作只是将引用和某个对象关联,假如同时有一个线程将引用指向另外一个 ...
- java CopyOnWriteArrayList的使用
除了加锁外,其实还有一种方式可以防止并发修改异常,这就是将读写分离技术(不是数据库上的). 先回顾一下一个常识: 1.JAVA中“=”操作只是将引用和某个对象关联,假如同时有一个线程将引用指向另外一个 ...
- ArrayList和CopyOnWriteArrayList
这篇文章的目的如下: 了解一下ArrayList的增删改查实现原理 看看为什么说ArrayList查询快而增删慢? CopyOnWriteArrayList为什么并发安全且性能比Vector好 1. ...
- java基础解析系列(八)---fail-fast机制及CopyOnWriteArrayList的原理
fail-fast机制及CopyOnWriteArrayList的原理 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析系列( ...
随机推荐
- 【学习总结】Python-3-Python数字类型转换
菜鸟教程-Python3-Python数字 Python3支持三种数值类型:整型int,浮点型float,复数complex 格式:将数字类型作为函数名即可,然后传入要转换的参数. int(x) 将x ...
- MapReduce-WordCountDemo
/** * @Author: dreamer Q * @Date: 2019/11/4 22:26 * @Version 1.0 * @Discription 使用MapReduce 开发 WordC ...
- ajax中的application/x-www-form-urlencoded中的使用[转]
一,HTTP上传的基本知识 在Form元素的语法中,EncType表明提交数据的格式 用 Enctype 属性指定将数据回发到服务器时浏览器使用的编码类型.下边是说明: application/x-w ...
- linux处理器子系统调优
- Nginx学习总结:proxy与rewrite模块(三)
斜体下划线,表示建议采用默认配置,无需显式的配置 一.ngx_http_upstream_module 此模块中可配置的指令并不是很多.nginx的负载均衡算法包括: 1)round-robin:轮询 ...
- Elasticsearch单机安装_集群搭建_索引基本操作_Head插件安装与基本操作_ik分词器配置_logstash实现数据同步_教程
一.Elasticsearch单机安装 1.将es安装包传到服务器上 这是安装包 这里我是用的是WinSCP上传工具,上传到/home/plugins文件下. 进入Xshell,验证一下是否上传成功. ...
- 系列(1) Note.Js的安装和部署
一.检测电脑上是否安装node 首先确认下你的node是否已经安装并且配置到了环境变量中了,直接按下快捷键[win+R],调出运行框,然后在里面输入cmd,进入windows的命令窗口. 在命令窗口中 ...
- python之 matplotlib模块之基本三图形(直线,曲线,直方图,饼图)
matplotlib模块是python中一个强大的绘图模块 安装 pip install matplotlib 首先我们来画一个简单的图来感受它的神奇 import numpy as np impo ...
- js实现动态加载input 提示信息
思路:使用<datalist> 标签定义选项列表.请与 input 元素配合使用该元素,来定义 input 可能的值.datalist 及其选项不会被显示出来,它仅仅是合法的输入值列表.请 ...
- 【Shiro】SpringBoot集成Shiro
项目版本: springboot2.x shiro:1.3.2 Maven配置: <dependency> <groupId>org.apache.shiro</grou ...