一、摘要

在介绍 CopyOnWriteArrayList 之前,我们一起先来看看如下方法执行结果,代码内容如下:

public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("1");
System.out.println("原始list元素:"+ list.toString());
//通过对象移除等于内容为1的元素
for (String item : list) {
if("1".equals(item)) {
list.remove(item);
}
}
System.out.println("通过对象移除后的list元素:"+ list.toString());
}

执行结果内容如下:

原始list元素:[1, 2, 1]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.example.container.a.TestList.main(TestList.java:16)

很遗憾,结果并没有达到我们想要的预期效果,执行之后直接报错!抛ConcurrentModificationException异常!

为啥会抛这个异常呢?

我们一起来看看,foreach 写法实际上是对List.iterator() 迭代器的一种简写,因此我们可以从分析List.iterator() 迭代器进行入手,看看为啥会抛这个异常。

ArrayList类中的Iterator迭代器实现,源码内容:

通过代码我们发现 Itr 是 ArrayList 中定义的一个私有内部类,每次调用next、remove方法时,都会调用checkForComodification方法,源码如下:

/**修改次数检查*/
final void checkForComodification() {
//检查List中的修改次数是否与迭代器类中的修改次数相等
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

checkForComodification方法,实际上是用来检查List中的修改次数modCount是否与迭代器类中的修改次数expectedModCount相等,如果不相等,就会抛出ConcurrentModificationException异常!

那么问题基本上已经清晰了,上面的运行结果之所以会抛出这个异常,就是因为List中的修改次数modCount与迭代器类中的修改次数expectedModCount不相同造成的!

阅读过集合源码的朋友,可能想起Vector这个类,它不是 JDK 中 ArrayList 线程安全的一个版本么?

好的,为了眼见为实,我们把ArrayList换成Vector来测试一下,代码如下:

public static void main(String[] args) {
Vector<String> list = new Vector<String>();
//模拟10个线程向list中添加内容,并且读取内容
for (int i = 0; i < 5; i++) {
final int j = i;
new Thread(new Runnable() {
@Override
public void run() {
//添加内容
list.add(j + "-j"); //读取内容
for (String str : list) {
System.out.println("内容:" + str);
}
}
}).start();
}
}

执行程序,运行结果如下:

还是一样的结果,抛异常了,Vector虽然线程安全,只不过是加了synchronized关键字,但是迭代问题完全没有解决!

继续回到本文要介绍的 CopyOnWriteArrayList 类,我们把上面的例子,换成CopyOnWriteArrayList类来试试,源码内容如下:

public static void main(String[] args) {
//将ArrayList换成CopyOnWriteArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
list.add("1");
System.out.println("原始list元素:"+ list.toString()); //通过对象移除等于11的元素
for (String item : list) {
if("1".equals(item)) {
list.remove(item);
}
}
System.out.println("通过对象移除后的list元素:"+ list.toString());
}

执行结果如下:

原始list元素:[1, 2, 1]
通过对象移除后的list元素:[2]

呃呵,执行成功了,没有报错!是不是很神奇~~

当然,类似上面这样的例子有很多,比如写10个线程向list中添加元素读取内容,也会抛出上面那个异常,操作如下:

public static void main(String[] args) {
final List<String> list = new ArrayList<>();
//模拟10个线程向list中添加内容,并且读取内容
for (int i = 0; i < 10; i++) {
final int j = i;
new Thread(new Runnable() {
@Override
public void run() {
//添加内容
list.add(j + "-j"); //读取内容
for (String str : list) {
System.out.println("内容:" + str);
}
}
}).start();
}
}

类似的操作例子就非常多了,这里就不一一举例了。

CopyOnWriteArrayList 实际上是 ArrayList 一个线程安全的操作类!

从它的名字可以看出,CopyOnWrite 是在写入的时候,不修改原内容,而是将原来的内容复制一份到新的数组,然后向新数组写完数据之后,再移动内存指针,将目标指向最新的位置。

二、简介

从 JDK1.5 开始 Java 并发包里提供了两个使用CopyOnWrite 机制实现的并发容器,分别是CopyOnWriteArrayList和CopyOnWriteArraySet 。

从名字上看,CopyOnWriteArrayList主要针对动态数组,一个线程安全版本的 ArrayList !

而CopyOnWriteArraySet主要针对集,CopyOnWriteArraySet可以理解为HashSet线程安全的操作类,我们都知道HashSet基于散列表HashMap实现,但是CopyOnWriteArraySet并不是基于散列表实现,而是基于CopyOnWriteArrayList动态数组实现!

关于这一点,我们可以从它的源码中得出结论,部分源码内容:

从源码上可以看出,CopyOnWriteArraySet默认初始化的时候,实例化了CopyOnWriteArrayList类,CopyOnWriteArraySet的大部分方法,例如add、remove等方法都基于CopyOnWriteArraySet实现!

两者最大的不同点是,CopyOnWriteArrayList可以允许元素重复,而CopyOnWriteArraySet不允许有重复的元素!

好了,继续来 BB 本文要介绍的CopyOnWriteArrayList类~~

打开CopyOnWriteArrayList类的源码,内容如下:

可以看到 CopyOnWriteArrayList 的存储元素的数组array变量,使用了volatile关键字保证的多线程下数据可见行;同时,使用了ReentrantLock可重入锁对象,保证线程操作安全。

在初始化阶段,CopyOnWriteArrayList默认给数组初始化了一个对象,当然,初始化方法还有很多,比如如下我们经常会用到的一个初始化方法,源码内容如下:

这个方法,表示如果我们传入的是一个 ArrayList数组对象,会将对象内容复制一份到新的数组中,然后初始化进去,操作如下:

List<String> list = new ArrayList<>();
...
//CopyOnWriteArrayList将list内容复制出来,并创建一个新的数组
CopyOnWriteArrayList<String> copyList = new CopyOnWriteArrayList<>(list);

CopyOnWriteArrayList是对原数组内容进行复制再写入,那么是不是也存在多线程下操作也会发生冲突呢?

下面我们再一起来看看它的方法实现!

三、常用方法

3.1、添加元素

add()方法是CopyOnWriteArrayList的添加元素的入口!

CopyOnWriteArrayList之所以能保证多线程下安全操作, add()方法功不可没,源码如下:

操作步骤如下:

  • 1、获得对象锁;
  • 2、获取数组内容;
  • 3、将原数组内容复制到新数组;
  • 4、写入数据;
  • 5、将array数组变量地址指向新数组;
  • 6、释放对象锁;

在 Java 中,独占锁方面,有2种方式可以保证线程操作安全,一种是使用虚拟机提供的synchronized 来保证并发安全,另一种是使用JUC包下的ReentrantLock可重入锁来保证线程操作安全。

CopyOnWriteArrayList使用了ReentrantLock这种可重入锁,保证了线程操作安全,同时数组变量array使用volatile保证多线程下数据的可见行!

其他的,还有指定下标进行添加的方法,如add(int index, E element),操作类似,先找到需要添加的位置,如果是中间位置,则以添加位置为分界点,分两次进行复制,最后写入数据!

3.2、移除元素

remove()方法是CopyOnWriteArrayList的移除元素的入口!

源码如下:

操作类似添加方法,步骤如下:

  • 1、获得对象锁;
  • 2、获取数组内容;
  • 3、判断移除的元素是否为数组最后的元素,如果是最后的元素,直接将旧元素内容复制到新数组,并重新设置array值;
  • 4、如果是中间元素,以index为分界点,分两节复制;
  • 5、将array数组变量地址指向新数组;
  • 6、释放对象锁;

当然,移除的方法还有基于对象的remove(Object o),原理也是一样的,先找到元素的下标,然后执行移除操作。

3.3、查询元素

get()方法是CopyOnWriteArrayList的查询元素的入口!

源码如下:

public E get(int index) {
//获取数组内容,通过下标直接获取
return get(getArray(), index);
}

查询因为不涉及到数据操作,所以无需使用锁进行处理!

3.4、遍历元素

上文中我们介绍到,基本都是在遍历元素的时候因为修改次数与迭代器中的修改次数不一致,导致检查的时候抛异常,我们一起来看看CopyOnWriteArrayList迭代器实现。

打开源码,可以得出CopyOnWriteArrayList返回的迭代器是COWIterator,源码如下:

public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}

打开COWIterator类,其实它是CopyOnWriteArrayList的一个静态内部类,源码如下:

可以看出,在使用迭代器的时候,遍历的元素都来自于上面的getArray()方法传入的对象数组,也就是传递进来的 array 数组!

由此可见,CopyOnWriteArrayList 在使用迭代器遍历的时候,操作的都是原数组,没有像上面那样进行修改次数判断,所以不会抛异常!

当然,从源码上也可以得出,使用CopyOnWriteArrayList的迭代器进行遍历元素的时候,不能调用remove()方法移除元素,因为不支持此操作!

如果想要移除元素,只能使用CopyOnWriteArrayList提供的remove()方法,而不是迭代器的remove()方法,这个需要注意一下!

四、总结

CopyOnWriteArrayList是一个典型的读写分离的动态数组操作类!

在写入数据的时候,将旧数组内容复制一份出来,然后向新的数组写入数据,最后将新的数组内存地址返回给数组变量;移除操作也类似,只是方式是移除元素而不是添加元素;而查询方法,因为不涉及线程操作,所以并没有加锁出来!

因为CopyOnWriteArrayList读取内容没有加锁,在写入数据的时候同时也可以进行读取数据操作,因此性能得到很大的提升,但是也有缺陷,对于边读边写的情况,不一定能实时的读到最新的数据,比如如下操作:

public static void main(String[] args) throws InterruptedException {
final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
for (int i = 0; i < 5; i++) {
final int j =i;
new Thread(new Runnable() {
@Override
public void run() {
//写入数据
list.add("i-" + j);
//读取数据
for (String str : list) {
System.out.println("线程-" + Thread.currentThread().getName() + ",读取内容:" + str);
}
}
}).start();
}
}

新建5个线程向list中添加元素,执行结果如下:

可以看到,5个线程的读取内容有差异!

因此CopyOnWriteArrayList很适合读多写少的应用场景!

五、参考

1、JDK1.7&JDK1.8 源码

2、掘金 - 拥抱心中的梦想 - 说一说Java中的CopyOnWriteArrayList

面试必问之 CopyOnWriteArrayList,你了解多少?的更多相关文章

  1. linux驱动工程面试必问知识点

    linux内核原理面试必问(由易到难) 简单型 1:linux中内核空间及用户空间的区别?用户空间与内核通信方式有哪些? 2:linux中内存划分及如何使用?虚拟地址及物理地址的概念及彼此之间的转化, ...

  2. 互联网公司面试必问的Redis题目

    Redis是一个非常火的非关系型数据库,火到什么程度呢?只要是一个互联网公司都会使用到.Redis相关的问题可以说是面试必问的,下面我从个人当面试官的经验,总结几个必须要掌握的知识点. 介绍:Redi ...

  3. 【面试必问】python实例方法、类方法@classmethod、静态方法@staticmethod和属性方法@property区别

    [面试必问]python实例方法.类方法@classmethod.静态方法@staticmethod和属性方法@property区别 1.#类方法@classmethod,只能访问类变量,不能访问实例 ...

  4. 互联网公司面试必问的mysql题目(下)

    这是mysql系列的下篇,上篇文章地址我附在文末. 什么是数据库索引?索引有哪几种类型?什么是最左前缀原则?索引算法有哪些?有什么区别? 索引是对数据库表中一列或多列的值进行排序的一种结构.一个非常恰 ...

  5. 互联网公司面试必问的mysql题目(上)

    又到了招聘的旺季,被要求准备些社招.校招的题库.(如果你是应届生,尤其是东北的某大学,绝对福利哦) 介绍:MySQL是一个关系型数据库管理系统,目前属于 Oracle 旗下产品.虽然单机性能比不上or ...

  6. 面试必问:JVM类加载机制详细解析

    前言 在Java面试中,简历上有写JVM(Java虚拟机)相关的东西,JVM的类加载机制基本是面试必问的知识点. 类的加载和卸载 JVM是虚拟机的一种,它的指令集语言是字节码,字节码构成的文件是cla ...

  7. 一线大厂Java面试必问的2大类Tomcat调优

    一.前言 最近整理了 Tomcat 调优这块,基本上面试必问,于是就花了点时间去搜集一下 Tomcat 调优都调了些什么,先记录一下调优手段,更多详细的原理和实现以后用到时候再来补充记录,下面就来介绍 ...

  8. python笔记39-unittest框架如何将上个接口的返回结果给下个接口适用(面试必问)

    前言 面试必问:如何将上个接口的返回结果,作为下个接口的请求入参?使用unittest框架写用例时,如何将用例a的结果,给用例b使用. unittest框架的每个用例都是独立的,测试数据共享的话,需设 ...

  9. 高级测试工程师面试必问面试基础整理——python基础(一)(首发公众号:子安之路)

    现在深圳市场行情,高级测试工程师因为都需要对编程语言有较高的要求,但是大部分又没有python笔试机试题,所以面试必问python基础,这里我整理一下python基本概念,陆续收集到面试中python ...

随机推荐

  1. Python pyecharts绘制仪表盘

    一.仪表盘gauge.add方法简介 gauge.add()方法签名 add(name,attr,value, scale_range=none, angle_range=none,**kwargs) ...

  2. 用Word做表,总会多出一页来?(Word技巧集团)

    用Word做表的同学都有这样的体验,表格后面都会多出一行,默认的,去都去不掉.如果一个表刚好做到一页的最下面,嗯,那就多出一页来,如下图: 这一页,多不多余啊,讨不讨厌啊,可不可恶啊--可是Word同 ...

  3. Lookup函数(Excel函数集团)

    此处文章均为本妖原创,供下载.学习.探讨! 文章下载源是Office365国内版1Driver,如有链接问题请联系我. 请勿用于商业!谢谢 下载地址:https://officecommunity-m ...

  4. 四、Uniapp+vue+腾讯IM+腾讯音视频开发仿微信的IM聊天APP,支持各类消息收发,音视频通话,附vue实现源码(已开源)-会话好友列表的实现

    会话好友列表的实现 1.项目引言 2.腾讯云后台配置TXIM 3.配置项目并实现IM登录 4.会话好友列表的实现 5.聊天输入框的实现 6.聊天界面容器的实现 7.聊天消息项的实现 8.聊天输入框扩展 ...

  5. c++读取文件操作和写入文件

    在C++中与读取文件和写入文件简单操作有关的类分别有ifstream(文件读入).ofstream(文件写出).fstream (文件读入和写出). 名称 作用 ifstream 文件读入 ofstr ...

  6. 【LeetCode】1200. Minimum Absolute Difference 解题报告 (C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 排序 日期 题目地址:https://leetcode ...

  7. 【LeetCode】1150. Check If a Number Is Majority Element in a Sorted Array 解题报告(C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 字典 二分查找 日期 题目地址:https://lee ...

  8. Adversarial Detection methods

    目录 Kernel Density (KD) Local Intrinsic Dimensionality (LID) Gaussian Discriminant Analysis (GDA) Gau ...

  9. Accelerating Deep Learning by Focusing on the Biggest Losers

    目录 概 相关工作 主要内容 代码 Accelerating Deep Learning by Focusing on the Biggest Losers 概 思想很简单, 在训练网络的时候, 每个 ...

  10. G2022 次列车即将进站,前端人请抓紧时间上车

    前端发展百花放,一技未熟百技出.未知何处去下手,关注小编胜百书. 我是前端人,关注小编,分享更多前端相关知识! 平安夜一个人?圣诞一个人? 跨年还是一个人吗? 没事,关注小编,每个节日让小编陪你一起度 ...