https://mp.weixin.qq.com/s/nvOhOPp9_wQXP6sTcjuqlQ java
相信你们对 ConcurrentHashMap 这个线程安全类很是熟悉,可是若是我想在多线程环境下使用 ArrayList,该怎么处理呢?阿粉今天来给你揭晓答案!数组
在介绍 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异常!并发
为啥会抛这个异常呢?ide
咱们一块儿来看看,foreach
写法其实是对List.iterator()
迭代器的一种简写,所以咱们能够从分析List.iterator()
迭代器进行入手,看看为啥会抛这个异常。性能
ArrayList
类中的Iterator
迭代器实现,源码内容:测试
经过代码咱们发现 Itr
是 ArrayList
中定义的一个私有内部类,每次调用next
、remove
方法时,都会调用checkForComodification
方法,源码以下:spa
/**修改次数检查*/
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
是对原数组内容进行复制再写入,那么是否是也存在多线程下操做也会发生冲突呢?
下面咱们再一块儿来看看它的方法实现!
add()
方法是CopyOnWriteArrayList
的添加元素的入口!
CopyOnWriteArrayList
之因此能保证多线程下安全操做, add()
方法功不可没,源码以下:
操做步骤以下:
在 Java 中,独占锁方面,有2种方式能够保证线程操做安全,一种是使用虚拟机提供的synchronized
来保证并发安全,另外一种是使用JUC
包下的ReentrantLock
可重入锁来保证线程操做安全。
CopyOnWriteArrayList
使用了ReentrantLock
这种可重入锁,保证了线程操做安全,同时数组变量array
使用volatile
保证多线程下数据的可见性!
其余的,还有指定下标进行添加的方法,如add(int index, E element)
,操做相似,先找到须要添加的位置,若是是中间位置,则以添加位置为分界点,分两次进行复制,最后写入数据!
remove()
方法是CopyOnWriteArrayList
的移除元素的入口!
源码以下:
操做相似添加方法,步骤以下:
array
值;index
为分界点,分两节复制;固然,移除的方法还有基于对象的remove(Object o)
,原理也是同样的,先找到元素的下标,而后执行移除操做。
get()
方法是CopyOnWriteArrayList
的查询元素的入口!
源码以下:
public E get(int index) {
//获取数组内容,经过下标直接获取
return get(getArray(), index);
}
查询由于不涉及到数据操做,因此无需使用锁进行处理!
上文中咱们介绍到,基本都是在遍历元素的时候由于修改次数与迭代器中的修改次数不一致,致使检查的时候抛异常,咱们一块儿来看看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
很适合读多写少的应用场景!
一、JDK1.7&JDK1.8 源码
二、掘金 - 拥抱心中的梦想 - 说一说Java中的CopyOnWriteArrayList