简介

  我们都很熟悉容器对象ArrayList,并且在初学时就被告知ArrayList不是线程安全的:当我们在使用迭代器遍历ArrayList时,如果有其他线程修改了ArrayList对象,那么就会抛出ConcurrentModificationException异常。相较于Vector使用synchronized加锁保证线程安全性,JUC提供了多线程版“ArrayList”:CopyOnWriteArrayList。下面是JDK对CopyOnWriteArrayList的介绍:

A thread-safe variant of ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array.

This is ordinarily too costly, but may be more efficient than alternatives when traversal operations vastly outnumber mutations, and is useful when you cannot or don't want to synchronize traversals, yet need to preclude interference among concurrent threads. The "snapshot" style iterator method uses a reference to the state of the array at the point that the iterator was created. This array never changes during the lifetime of the iterator, so interference is impossible and the iterator is guaranteed not to throw ConcurrentModificationException. The iterator will not reflect additions, removals, or changes to the list since the iterator was created. Element-changing operations on iterators themselves (remove, set, and add) are not supported. These methods throw UnsupportedOperationException.

大意是CopyOnWriteArrayList是线程安全版本的ArrayList,所有对CopyOnWriteArrayList修改的操作都是在内部数组的拷贝上进行操作的,这样做虽然内存花费大,但是在遍历操作大于修改操作时这样效率更高,可以有效防止抛出ConcurrentModificationException异常,迭代器迭代期间不支持对元素更改操作,否则会抛出UnsupportedOperationException异常。

类结构

  CopyOnWriteArrayList实现了List接口,List表示是有序的Collection,即它用某种特定的插入顺序来维护元素顺序;实现了标记接口RandomAccess接口支持快速访问;实现了Iterable接口可以使用迭代器遍历容器元素。

源码解析

构造方法

  CopyOnWriteArrayList互斥锁用于对修改容器元素阶段加锁,被volatile修饰的Object数组是CopyOnWriteArrayList存储数据的底层数据结构,通过volatile保证能够读到其他线程对CopyOnWriteArrayList数据的修改,对于数组的访问都是通过getArray/setArray方法。

  CopyOnWriteArrayList提供了三个重载的构造函数,无参构造函数会调用setArray方法构造一个空的Object数组,另外两个构造函数分别传入集合/数组参数,将集合/数组内元素存入CopyOnWriteArrayList的底层Object数组。

public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L; //互斥锁
final transient ReentrantLock lock = new ReentrantLock(); //底层存储数据数组,只能通过getArray/setArray访问设置,volatile动态数组
private transient volatile Object[] array; final Object[] getArray() {
return array;
} final void setArray(Object[] a) {
array = a;
} public CopyOnWriteArrayList() {
setArray(new Object[0]);
} //传入Collection集合对象,将集合中元素存入CopyOnWriteArrayList
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
} //传入数组
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
}

add(E e)

   add方法的作用是把传入元素添加到链表list的末尾。add方法有两点需要注意:1.在写入过程使用了互斥锁,所以同一时间只有一个线程在修改CopyOnWriteArrayList 2.增加元素并不是直接在原数组操作,而是在原数组的拷贝数组上添加元素的,添加完成后再调用setArray方法用新数组代替原始数组

    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();
}
}

add(int index, E element)

  这个方法的作用是把新元素插入到特定位置,会把原来位置的元素向后挤。过程与上面的add大致相同。

    public void add(int index, E element) {
//互斥锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//原始数组
Object[] elements = getArray();
int len = elements.length;
//检查index有效性
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
//拷贝数组
Object[] newElements;
//从index到数组末尾要向后移动一位数组元素的个数
int numMoved = len - index;
//如果index==length,直接把原数组复制到新数组
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
//否则分成两段复制,原始数组index前面的元素位置一一对应赋值到新数组,原数组index开始的元素复制到
//新数组index+1到length+1,相当于依次后移。空出来的index就是新元素插入的位置
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
//插入新元素
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}

get(int index)

  获取索引位置为index位置处的元素,获取元素过程中没有使用互斥锁上锁。

    public E get(int index) {
return get(getArray(), index);
} @SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
//返回数组index处位置
return (E) a[index];
}

remove(int index)

  移除index处元素。由于涉及到修改到对链表内元素的修改,因此移除过程会使用互斥锁上锁。

    public E remove(int index) {
//上锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//原始数组
Object[] elements = getArray();
int len = elements.length;
//数组index处要移除的元素
E oldValue = get(elements, index);
//index+1到数组末尾要移动的元素个数
int numMoved = len - index - 1;
//如果要移除的元素在数组末尾(index=len-1),直接复制数组区间[0,len-2]所有元素到新数组
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
//如果移除的元素不再末尾,分成两段赋值,首先把[0,index-1]区间元素复制到新数组,再把
//[index+1,len-1]复制到新数组
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}

迭代器Iterator遍历

  CopyOnWriteArrayList支持使用迭代器迭代,使用iterator方法返回COWIterator对象,在迭代过程中没有上锁,也不支持remove/set/add等修改方法。

	//返回COWIterator对象
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
} //实现迭代器的内部类
static final class COWIterator<E> implements ListIterator<E> {
//遍历时原始数组的快照
private final Object[] snapshot;
//迭代器迭代的游标
private int cursor; private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
} public boolean hasNext() {
return cursor < snapshot.length;
} public boolean hasPrevious() {
return cursor > 0;
} @SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
} @SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
} public int nextIndex() {
return cursor;
}
}

总结

  Copy-On-Write简称COW,中文简称写入时复制,是一种程序设计优化策略,具体思想就是对于共享内容做修改操作时,会把共享内容复制出来,在复制内容上修改,修改完成后在返还到原内容上。对于CopyOnWriteArrayList而言,向容器添加元素是先把容器复制一份,向复制的容器添加元素,添加成功后把复制的容器赋值给原容器对象;而对于读取容器的操作直接在原容器进行操作。CopyOnWriteArrayList利用了COW技术实现读写的分离,对于写操作实行加锁保证安全性,读操作不改变容器不需加锁,相对于Vector对所有操作加锁来保证安全性的效率更高,适合于读多写少的场景。同时在CopyOnWriteArrayList保存数据量较大时,对于容器的写入由于复制原容器产生新容器用于写操作,造成了两倍的内存消耗,会引发频繁的垃圾回收,降低性能。

多线程十之CopyOnWriteArrayList源码分析的更多相关文章

  1. java多线程系列(九)---ArrayBlockingQueue源码分析

    java多线程系列(九)---ArrayBlockingQueue源码分析 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 j ...

  2. CopyOnWriteArrayList 源码分析 基于jdk1.8

    CopyOnWriteArrayList  源码分析: 1:成员属性: final transient ReentrantLock lock = new ReentrantLock();  //内部是 ...

  3. Alink漫谈(二十二) :源码分析之聚类评估

    Alink漫谈(二十二) :源码分析之聚类评估 目录 Alink漫谈(二十二) :源码分析之聚类评估 0x00 摘要 0x01 背景概念 1.1 什么是聚类 1.2 聚类分析的方法 1.3 聚类评估 ...

  4. CopyOnWriteArrayList源码分析

    基于jdk1.7源码 一.无锁容器 CopyOnWriteArrayList是JDK5中添加的新的容器,除此之外,还有CopyOnWriteArraySet.ConcurrentHahshMap和Co ...

  5. Java并发编程笔记之CopyOnWriteArrayList源码分析

    并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝 ...

  6. java多线程系列:ThreadPoolExecutor源码分析

    前言 这篇主要讲述ThreadPoolExecutor的源码分析,贯穿类的创建.任务的添加到线程池的关闭整个流程,让你知其然所以然.希望你可以通过本篇博文知道ThreadPoolExecutor是怎么 ...

  7. Java多线程学习之ThreadLocal源码分析

    0.概述 ThreadLocal,即线程本地变量,是一个以ThreadLocal对象为键.任意对象为值的存储结构.它可以将变量绑定到特定的线程上,使每个线程都拥有改变量的一个拷贝,各线程相同变量间互不 ...

  8. 死磕 java集合之CopyOnWriteArrayList源码分析

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 CopyOnWriteArrayList是ArrayList的线程安全版本,内部也是通过 ...

  9. 【JUC】8.CopyOnWriteArrayList源码分析

    CopyOnWriteArrayList 解决脏读问题:牺牲写的效率,提高读的效率 CopyOnWriteArrayList是一种读写分离的思想体现的ArrayList: 它将读写的操作对象分离开来: ...

随机推荐

  1. 【洛谷P1126】机器人搬重物

    题目大意:给定一个 N 行,M 列的地图,一个直径为 1.6 的圆形机器人需要从起点走到终点,每秒钟可以实现:向左转,向右转,向前 1-3 步.求从起点到终点最少要多长时间. 题解:相比于普通的走迷宫 ...

  2. C++并发编程之std::future

    简单地说,std::future 可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段.std::future 通常由某个 Provider 创建,你可以把 Provider 想象成 ...

  3. 使用开源Breeze工具部署Kubernetes 1.12.1高可用集群

    Breeze项目是深圳睿云智合所开源的Kubernetes图形化部署工具,大大简化了Kubernetes部署的步骤,其最大亮点在于支持全离线环境的部署,且不需要FQ获取Google的相应资源包,尤其适 ...

  4. Codeforces 931 C. Laboratory Work

    http://codeforces.com/problemset/problem/931/C 题意: 给定一个数列,要求构造一个等长的数列,使得数列的平均值等于给定数列,并且使得构造出的数列中与原数列 ...

  5. Codeforces 295 B. Greg and Graph

    http://codeforces.com/problemset/problem/295/B 题意: 给定一个有边权的有向图.再给定一个1~n的排列. 按排列中的顺序依次删除点,问每次删除后,所有点对 ...

  6. springmvc常用注解标签详解-推荐

    1.@Controller 在SpringMVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ...

  7. mybatis dao 层开发简易版 非整合 spring

    同样老习惯,先上项目结构截图 首先 补充上篇文中缺失的 mysql demo 用的 小脚本 drop database if exists mybatis; CREATE DATABASE `myba ...

  8. 滚动视差插件skrollr.js

    东西虽好,但也不能懒到自己一点都不去做总结,那么下方将会写出从网上找到,比较好的网址(应该是根据官网翻译的). 自己先做一个总结:这个插件兼容上做到了降级处理,虽然低端浏览器没有那么顺畅的效果,但是勉 ...

  9. HDU 1867 A + B for you again 字符匹配

    解题报告:给你两个字符串,让你连接起来,没有前后顺序,要求是长度最短优先,其次是字典序最小.这题我用的是KMP,做两次匹配,分别把第一次跟第二次输入的字符串放前面,然后比较两次得到的字符窜的长度和字典 ...

  10. NOIP2016-D2-T2 蚯蚓(单调队列)

    构建三个单调队列(用STL),分别储存未切的蚯蚓,切后的第一段,切后的第二段,即可简单证明其单调性. 证明:设$q$为单调队列$\because a_1 \geqslant a_2 \geqslant ...