简介

  我们都很熟悉容器对象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. 解题:九省联考2018 秘密袭击CoaT

    题面 按照*Miracle*的话来说,网上又多了一篇n^3暴力的题解 可能是因为很多猫题虽然很好,但是写正解性价比比较低? 直接做不可做,转化为统计贡献:$O(n)$枚举每个权值,直接统计第k大大于等 ...

  2. 解题:九省联考2018 IIIDX

    题面 我当时在考场上划水的时候好像乱搞搞了20pts,然后发现一堆同届的都写了55pts的贪心=.=??? 那就先说那55pts的贪心吧,这个现在看起来还是非常显然的,就是按题意来每一块是分属一个点的 ...

  3. 《剑指offer》— JavaScript(30)连续子数组的最大和

    连续子数组的最大和 题目描述 HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学.今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好 ...

  4. jedisClient操作redis实现增删改查功能

    这个集群环境下和单机环境下: package com.taotao.sso.dao.impl; import org.springframework.beans.factory.annotation. ...

  5. java基础基础总结----- 随机数(产生四个随机数)

    前言:在开发的时候经常会遇见,一些验证码登录,其实这些东西,很简单.我曾经开发过一个验证码登录的页面,那时用的插件.但是作为一个合格的开发者,要了解其内部的核心知识,有些东西,可以不深入了解,但是要做 ...

  6. python爬虫requests过程中添加headers

    浏览器中打开页面,以edge为例,点击“查看源”或F12 第一步:点击上图中“网络”标签,然后刷新或载入页面 第二步:在右侧“标头”下方的“请求标头”中的所有信息都是headers内容,添加到requ ...

  7. .net 未被引用的错误

    开发的时候遇到了一个错误,如下: 错误 1 类型“System.ServiceModel.ClientBase`1<T0>”在未被引用的程序集中定义. 我原本以为是版本号的问题,添加了引用 ...

  8. SQL记录-PLSQL-DBMS输出

    PL/SQL DBMS输出   DBMS_OUTPUT是一个内置的软件包,能够显示输出显示调试信息,并从PL/ SQL块,子程序,包和触发器发送消息.我们已经使用这个包在我们所有的教程中. 让我们来看 ...

  9. Spring RedisTemplate操作-xml配置(1)

    网上没能找到全的spring redistemplate操作例子,故特意化了点时间做了接口调用练习,基本包含了所有redistemplate方法. 该操作例子是个系列,该片为spring xml配置, ...

  10. livereload使用方法

    搞这个自动刷新的插件搞了好几个小时了还没搞明白,快被气死了,想改用browser-sync结果npm又一直转啊转一直卡死. 刚才终于神奇地搞定了,结果发现还是我自己智商太低...大概的经过是这样的.. ...