注:在看这篇文章之前,如果对ArrayList底层不清楚的话,建议先去看看ArrayList源码解析。

http://www.cnblogs.com/java-zhao/p/5102342.html

1、对于CopyOnWriteArrayList需要掌握以下几点

  • 创建:CopyOnWriteArrayList()
  • 添加元素:即add(E)方法
  • 获取单个对象:即get(int)方法
  • 删除对象:即remove(E)方法
  • 遍历所有对象:即iterator(),在实际中更常用的是增强型的for循环去做遍历

注:CopyOnWriteArrayList是一个线程安全,读操作时无锁的ArrayList。

2、创建

public CopyOnWriteArrayList()

使用方法:

List<String> list = new CopyOnWriteArrayList<String>();

相关源代码:

    private volatile transient Object[] array;//底层数据结构

    /**
* 获取array
*/
final Object[] getArray() {
return array;
} /**
* 设置Object[]
*/
final void setArray(Object[] a) {
array = a;
} /**
* 创建一个CopyOnWriteArrayList
* 注意:创建了一个0个元素的数组
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}

注意点:

  • 设置一个容量为0的Object[];ArrayList会创造一个容量为10的Object[]

3、添加元素

public boolean add(E e)

使用方法:

list.add("hello");

源代码:

    /**
* 在数组末尾添加元素
* 1)获取锁
* 2)上锁
* 3)获取旧数组及其长度
* 4)创建新数组,容量为旧数组长度+1,将旧数组拷贝到新数组
* 5)将要增加的元素加入到新数组的末尾,设置全局array为新数组
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;//这里为什么不直接用this.lock(即类中已经初始化好的锁)去上锁
lock.lock();//上锁
try {
Object[] elements = getArray();//获取当前的数组
int len = elements.length;//获取当前数组元素
/*
* Arrays.copyOf(elements, len + 1)的大致执行流程:
* 1)创建新数组,容量为len+1,
* 2)将旧数组elements拷贝到新数组,
* 3)返回新数组
*/
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;//新数组的末尾元素设成e
setArray(newElements);//设置全局array为新数组
return true;
} finally {
lock.unlock();//解锁
}
}

注意点:

  • Arrays.copyOf(T[] original, int newLength)该方法在ArrayList中讲解过

疑问:

  • 在add(E)方法中,为什么要重新定义一个ReentrantLock,而不直接使用那个定义的类变量锁(全局锁)

    • 答:事实上,按照他那样写,即使是在add、remove、set中存在多个引用,最后也是一个实例this.lock,所以不管你在add、remove、set中怎样去从新定义一个ReentrantLock,其实add、remove、set中最后使用的都是同一个锁this.lock,也就是说,同一时刻,add/remove/set只能有一个在运行。这样讲,就是说,下边这段代码完全可以做一个修改。修改前的代码:

          public boolean add(E e) {
      final ReentrantLock lock = this.lock;//这里为什么不直接用this.lock(即类中已经初始化好的锁)去上锁
      lock.lock();//上锁

      修改后的代码:

          public boolean add(E e) {
      //final ReentrantLock lock = this.lock;//这里为什么不直接用this.lock(即类中已经初始化好的锁)去上锁
      this.lock.lock();//上锁
  • 根据以上代码可知,每增加一个新元素,都要进行一次数组的复制消耗,那为什么每次不将数组的元素设大(比如说像ArrayList那样,设置为原来的1.5倍+1),这样就会大大减少因为数组元素复制所带来的消耗?

4、获取元素

public E get(int index)

使用方法:

list.get(0)

源代码:

    /**
* 根据下标获取元素
* 1)获取数组array
* 2)根据索引获取元素
*/
public E get(int index) {
return (E) (getArray()[index]);
}

注意点:

  • 获取不需要加锁

疑问:在《分布式Java应用:基础与实践》一书中作者指出:读操作会发生脏读,为什么?

从类属性部分,我们可以看到array数组是volatile修饰的,也就是当你对volatile进行写操作后,会将写过后的array数组强制刷新到主内存,在读操作中,当你读出数组(即getArray())时,会强制从主内存将array读到工作内存,所以应该不会发生脏读才对呀!!!

补:volatile的介绍见《附2 volatile》,链接如下:

http://www.cnblogs.com/java-zhao/p/5125698.html

5、删除元素

public boolean remove(Object o)

使用方法:

list.remove("hello")

源代码:

    /**
* 删除list中的第一个o
* 1)获取锁、上锁
* 2)获取旧数组、旧数组的长度len
* 3)如果旧数组长度为0,返回false
* 4)如果旧数组有值,创建新数组,容量为len-1
* 5)从0开始遍历数组中除了最后一个元素的所有元素
* 5.1)将旧数组中将被删除元素之前的元素复制到新数组中,
* 5.2)将旧数组中将被删除元素之后的元素复制到新数组中
* 5.3)将新数组赋给全局array
* 6)如果是旧数组的最后一个元素要被删除,则
* 6.1)将旧数组中将被删除元素之前的元素复制到新数组中
* 6.2)将新数组赋给全局array
*/
public boolean remove(Object o) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();//获取原数组
int len = elements.length;//获取原数组长度
if (len != 0) {//如果有数据
// Copy while searching for element to remove
// This wins in the normal case of element being present
int newlen = len - 1;//新数组长度为原数组长度-1
Object[] newElements = new Object[newlen];//创建新数组 for (int i = 0; i < newlen; ++i) {//遍历新数组(不包含最后一个元素)
if (eq(o, elements[i])) {
// 将旧数组中将被删除元素之后的元素复制到新数组中
for (int k = i + 1; k < len; ++k)
newElements[k - 1] = elements[k];
setArray(newElements);//将新数组赋给全局array
return true;
} else
newElements[i] = elements[i];//将旧数组中将被删除元素之前的元素复制到新数组中
} if (eq(o, elements[newlen])) {//将要删除的元素时旧数组中的最后一个元素
setArray(newElements);
return true;
}
}
return false;
} finally {
lock.unlock();
}
}

判断两个对象是否相等:

    /**
* 判断o1与o2是否相等
*/
private static boolean eq(Object o1, Object o2) {
return (o1 == null ? o2 == null : o1.equals(o2));
}

注意点:

  • 需要加锁
  • ArrayList的remove使用了System.arraycopy(这是一个native方法),而这里没使用,所以理论上这里的remove的性能要比ArrayList的remove要低

6、遍历所有元素

iterator()  hasNext()  next()

使用方法:

讲解用的:

        Iterator<String> itr = list.iterator();
while(itr.hasNext()){
System.out.println(itr.next());
}

实际中使用的:

        for(String str : list){
System.out.println(str);
}

源代码:

    public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
    private static 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;//0~snapshot.length-1
} public E next() {
if (!hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}

说明:这一块儿代码非常简单,看看代码注释就好。

注意:

由于遍历的只是全局数组的一个副本,即使全局数组发生了增删改变化,副本也不会变化,所以不会发生并发异常。但是,可能在遍历的过程中读到一些刚刚被删除的对象。

注意点:

总结:

  • 线程安全,读操作时无锁的ArrayList
  • 底层数据结构是一个Object[],初始容量为0,之后每增加一个元素,容量+1,数组复制一遍
  • 增删改上锁、读不上锁
  • 遍历过程由于遍历的只是全局数组的一个副本,即使全局数组发生了增删改变化,副本也不会变化,所以不会发生并发异常
  • 读多写少且脏数据影响不大的并发情况下,选择CopyOnWriteArrayList

疑问:

  • 每增加一个新元素,都要进行一次数组的复制消耗,那为什么每次不将数组的元素设大(比如说像ArrayList那样,设置为原来的1.5倍+1),这样就会大大减少因为数组元素复制所带来的消耗?
  • get(int)操作会发生脏读,为什么?

第三章 CopyOnWriteArrayList源码解析的更多相关文章

  1. 第三章 LinkedList源码解析

    一.对于LinkedList需要掌握的八点内容 LinkedList的创建:即构造器 往LinkedList中添加对象:即add(E)方法 获取LinkedList中的单个对象:即get(int in ...

  2. 第六章 ReentrantLock源码解析2--释放锁unlock()

    最常用的方式: int a = 12; //注意:通常情况下,这个会设置成一个类变量,比如说Segement中的段锁与copyOnWriteArrayList中的全局锁 final Reentrant ...

  3. 第十四章 Executors源码解析

    前边两章介绍了基础线程池ThreadPoolExecutor的使用方式.工作机理.参数详细介绍以及核心源码解析. 具体的介绍请参照: 第十二章 ThreadPoolExecutor使用与工作机理 第十 ...

  4. 第九章 LinkedBlockingQueue源码解析

    1.对于LinkedBlockingQueue需要掌握以下几点 创建 入队(添加元素) 出队(删除元素) 2.创建 Node节点内部类与LinkedBlockingQueue的一些属性 static ...

  5. 第四章 CopyOnWriteArraySet源码解析

    注:在看这篇文章之前,如果对CopyOnWriteArrayList底层不清楚的话,建议先去看看CopyOnWriteArrayList源码解析. http://www.cnblogs.com/jav ...

  6. ArrayList、CopyOnWriteArrayList源码解析(JDK1.8)

    本篇文章主要是学习后的知识记录,存在不足,或许不够深入,还请谅解. 目录 ArrayList源码解析 ArrayList中的变量 ArrayList构造函数 ArrayList中的add方法 Arra ...

  7. 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现

    写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...

  8. 第零章 dubbo源码解析目录

    第一章 第一个dubbo项目 第二章  dubbo内核之spi源码解析 2.1  jdk-spi的实现原理 2.2 dubbo-spi源码解析 第三章 dubbo内核之ioc源码解析 第四章 dubb ...

  9. 第十三章 ThreadPoolExecutor源码解析

    ThreadPoolExecutor使用方式.工作机理以及参数的详细介绍,请参照<第十二章 ThreadPoolExecutor使用与工作机理 > 1.源代码主要掌握两个部分 线程池的创建 ...

随机推荐

  1. 33. Search in Rotated Sorted Array旋转数组二分法查询

    一句话思路:反正只是寻找一个最小区间,断开也能二分.根据m第一次的落点,来分情况讨论. 一刷报错: 结构上有根本性错误:应该是while里面包括if,不然会把代码重复写两遍,不好. //situati ...

  2. 17-js 提交表单以及判空

    js

  3. shell 脚本 测试webApp

    vim **.sh文件 开头:#!/bin/bash ////////// copy cURL //因为这样copy的url就是一个命令(进入chrome的开发者工具里面,点network,找到刚刚访 ...

  4. VisulaVM 性能:分析 JVM 性能的免费工具

    VisualVM是一个集成多个JDK命令行工具的可视化工具.可以作为Java应用程序性能分析和运行监控的工具.开发人员可以利用它来监控.分 析线程信息,浏览内存堆数据.系统管理员可以利用它来监测.控制 ...

  5. Laravel 5.5 Api

    Laravel api token验证使用方法 从 Laravel 5.2 开始, Laravel 的将路由的配置进行了分拆, 在 routes 目录下有 web.php 和 api.php 两个路由 ...

  6. Java中通过SimpleDateFormat格式化当前时间:/** 输出格式:20060101010101001**/

    import java.util.*; import java.text.SimpleDateFormat; int y,m,d,h,mi,s,ms; String cur; Calendar cal ...

  7. Spring Boot集成Quartz注入Spring管理的类

    摘要: 在Spring Boot中使用Quartz时,在JOB中一般需要引用Spring管理的Bean,通过定义Job Factory实现自动注入. Spring有自己的Schedule定时任务,在S ...

  8. pageX/pageY,screenX/screenY,clientX/clientY的差别

    pageX/pageY,screenX/screenY,clientX/clientY的差别 $(document).click(function(e){ //x方向无差别 //alert(e.pag ...

  9. 33. Pay Gap for the Brightest Female Graduatea 最聪明的大学女毕业生面临的工资差距

    33. Pay Gap for the Brightest Female Graduatea 最聪明的大学女毕业生面临的工资差距 ① When young women were found to ma ...

  10. 1000多块整个插板,arduino + android 蓝牙插板的实现--屌丝版

       需求描述 儿子有一堆充电玩具,基本上都是锂电池,经常插上去充电忘了到时拔下来,所以需要一块能设置接通时间的插板以保障电池的安全.   硬件设计: 首先需要一块插板,接着需要一个继电器,然后采用a ...