1. 是什么

这个list借鉴的是读写分离的思想(弱一致性)

  • 读的时候可以并发读,不加锁;
  • 写的时候需要加锁,复制一份原有数据进行修改,改完后写回list

2. 如何使用

public class CopyOnWriteArrayListTest
{
public static void main(String[] args)
{
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("a");
list.add("b");
list.add("c"); list.remove("a");
System.out.println(list.contains("a"));//false System.out.println(list.get(0));//b list.set(0, "d"); System.out.println(list);//[d, c] System.out.println(Thread.currentThread().getName() + "读取list:" + list);//[d, c] new Thread(()->{
System.out.println(Thread.currentThread().getName() + "读取list:" + list);//[d, c]
try
{
TimeUnit.SECONDS.sleep(2);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "再次读取list:" + list);//[c] }).start();
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "读取list:" + list);//[d, c]
try
{
TimeUnit.SECONDS.sleep(2);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "再次读取list:" + list);//[c]
}).start(); try
{
TimeUnit.SECONDS.sleep(1);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
list.remove(0); System.out.println(Thread.currentThread().getName() + "读取修改后的list:" + list);//[c] }
}

3. 原理

3.1. 构造方法

public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable { //用于写时加锁
final transient ReentrantLock lock = new ReentrantLock(); //底层通过object数组实现, 通过 getArray/setArray访问
//volatile修饰保证多线程能及时看到最新结果
private transient volatile Object[] array; public CopyOnWriteArrayList() {
//初始化数组长度为0
setArray(new Object[0]);
} //get set都没做什么同步措施
final void setArray(Object[] a) {
array = a;
}
final Object[] getArray() {
return array;
}
}
  • 可以看到底层是通过object数组实现,

    • 通过 getArray/setArray访问,这两个方法都没做什么同步措施
    • 使用volatile修饰保证多线程能及时看到最新结果
  • 并且有一个ReentrantLock,保证写时加锁

3.2. add方法

public boolean add(E e) {
//多个线程同时调用add方法保证只有一个能进入
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();
}
}
  • 4行:加锁。写的操作必须加锁
  • 7-12行:copy原数组,扩容并修改
  • 14行:写回原数组

下面具体说明:

3.2.1. 先加锁

//多个线程同时调用add方法保证只有一个能进入
final ReentrantLock lock = this.lock;
lock.lock();
//。。。。。
} finally {
lock.unlock();
}

3.2.2. 复制数组并在这份数组上操作

//获取原数组
Object[] elements = getArray();
int len = elements.length;
//复制并扩容数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//赋值
newElements[len] = e;

3.2.3. 将复制的数组set回属性

//写回数组
//array = a;
//由于array是volatile修饰的,一旦改变之后其他线程会清空工作内存中的array,这样读取新值
setArray(newElements);

3.3. remove方法

  • remove
public boolean remove(Object o) {
//获取原数组
Object[] snapshot = getArray();
//从原数组的0-最后一个位置查找o是否存在并获取其下标
int index = indexOf(o, snapshot, 0, snapshot.length);
//存在则删除
return (index < 0) ? false : remove(o, snapshot, index);
}
  • 3-5行:找到数组中值o的位置index
  • 7行:加锁删除数组中的index位置

下面具体说明:

3.3.1. 找到要删除元素的位置

  • indexOf
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
//要找的值为null
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
//不为null
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}

这个方法没有加锁,只是把判断值是否为null分别进行处理

3.3.2. 加锁并把除了被删除之外的其他元素复制到新数组中,set回属性中

  • remove
//再snapshot数组删除下标为index且值为o的元素
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取现在的数组(可能已经改变过了)
Object[] current = getArray();
int len = current.length;
//数组改变过了
if (snapshot != current) findIndex: {
//遍历原数组和改变后的数组0-index位置是否相同,相同则可以删除
//有一个位置不相同 且 改变后的数组这个位置xx的元素与要删除的元素相同,那么重新从0-xx比较
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
//改变后的数组长度比index短了,那么不可能删除了,返回false
if (index >= len)
return false;
//改变后的数组index位置仍然是o
if (current[index] == o)
//退出findIndex这段代码,即执行34行
break findIndex;
//在改变后的数组中index-len位置重新寻找值为o的元素
index = indexOf(o, current, index, len);
//没找到,删除不了返回false
if (index < 0)
return false;
}
//创建原数组长度-1的新数组
Object[] newElements = new Object[len - 1];
//复制index左边的元素到新数组
System.arraycopy(current, 0, newElements, 0, index);
//复制index右边的元素到新数组(等于删除了index位置的元素)
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
//修改为新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

3.4. get(index)方法

public E get(int index) {
return get(getArray(), index);
}

get方法属于读取操作,所以不需要加锁,直接原数组上通过下标获取

3.4.1. 没加锁,通过数组[index]获取

  • get
private E get(Object[] a, int index) {
return (E) a[index];
}

3.5. contains

public boolean contains(Object o) {
//获取原数组
Object[] elements = getArray();
//再查找下标
return indexOf(o, elements, 0, elements.length) >= 0;
}
  • 3行:contains方法属于读方法,所以没加锁
  • 5行:直接遍历数组查找

3.5.1. 不加锁遍历数据查询

  • indexOf
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
//要找的值为null
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
//要找的值不为null
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}

3.6. set

//修改index位置的值为element
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取原数组
Object[] elements = getArray();
//获取原数组index位置的值
E oldValue = get(elements, index); //index位置不是element
if (oldValue != element) {
//复制原数组到新数组,修改index位置的值,set回array
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
//index位置已经是element了,那么直接set回array(保证volatile语义)
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
  • 4行:set方法属于写方法,所以需要先加锁
  • 7-9行:通过下标查找值
  • 12-18行:第2步查找到的值跟现在数组中的值不同,说明有改变。那么copy原数组、修改、写回原数组
  • 19-22 行:第2步查找到的值跟现在数组中的值相同,说明没有改变。虽然不做什么处理也行,但为了保证volatile语义,还是set回数组

下面详细说明:

3.6.1. 先加锁

final ReentrantLock lock = this.lock;
lock.lock();
try {
} finally {
lock.unlock();
}

3.6.2. 从原数组中获取index位置的值

//获取原数组
Object[] elements = getArray();
//获取原数组index位置的值
E oldValue = get(elements, index);

3.6.3. 如果index位置不相等,那么复制新数组、修改并set回属性

//index位置不是element
if (oldValue != element) {
//复制原数组到新数组,修改index位置的值,set回array
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements); }

3.6.4. 即使相等也要set回去保证volatile语义

//index位置已经是element了,那么直接set回array(保证volatile语义)
else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}

4. 总结

  • 适合读多写少的场景
  • 当调用add方法的时候加锁修改,内存中有两份数组,一份原始的,另一份是当前线程修改的数组
  • 当调用get方法时不加锁,获取的是原有的数组

5. 参考

Java源码分析系列笔记-10.CopyOnWriteArrayList的更多相关文章

  1. Java源码分析系列之HttpServletRequest源码分析

    从源码当中 我们可以 得知,HttpServletRequest其实 实际上 并 不是一个类,它只是一个标准,一个 接口而已,它的 父类是ServletRequest. 认证方式 public int ...

  2. Java源码分析系列

    1) 深入Java集合学习系列:HashMap的实现原理 2) 深入Java集合学习系列:LinkedHashMap的实现原理 3) 深入Java集合学习系列:HashSet的实现原理 4) 深入Ja ...

  3. spring源码分析系列 (2) spring拓展接口BeanPostProcessor

    Spring更多分析--spring源码分析系列 主要分析内容: 一.BeanPostProcessor简述与demo示例 二.BeanPostProcessor源码分析:注册时机和触发点 (源码基于 ...

  4. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  5. spring源码分析系列 (8) FactoryBean工厂类机制

    更多文章点击--spring源码分析系列 1.FactoryBean设计目的以及使用 2.FactoryBean工厂类机制运行机制分析 1.FactoryBean设计目的以及使用 FactoryBea ...

  6. Spring mvc源码分析系列--Servlet的前世今生

    Spring mvc源码分析系列--Servlet的前世今生 概述 上一篇文章Spring mvc源码分析系列--前言挖了坑,但是由于最近需求繁忙,一直没有时间填坑.今天暂且来填一个小坑,这篇文章我们 ...

  7. jQuery源码分析系列

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...

  8. MyCat源码分析系列之——结果合并

    更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...

  9. MyCat源码分析系列之——BufferPool与缓存机制

    更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...

  10. jquery2源码分析系列

    学习jquery的源码对于提高前端的能力很有帮助,下面的系列是我在网上看到的对jquery2的源码的分析.等有时间了好好研究下.我们知道jquery2开始就不支持IE6-8了,从jquery2的源码中 ...

随机推荐

  1. 网络编程-关闭连接(2)-Java的NIO在关闭socket时,究竟用了哪个系统调用函数?

    背景 在上一讲网络编程-关闭连接-C/C++相关系统调用中,提到过,目前项目使用Netty框架来实现的网络编程,查看netty源码可以得知,netty最终是调用了java Nio的close接口做的关 ...

  2. 【Java】Math类的基本操作

    Math类 Math 类是数学操作类,提供了一系列的数学操作方法,包括求绝对值.三角函数等,在 Math 类中提供的一切方法都是静态方法(类方法),所以直接由类名称调用即可. Math类的基本操作: ...

  3. 3D Gaussian 三维视觉重建

    论文资料 论文 https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/3d_gaussian_splatting_low.pdf 资料网站 ...

  4. pdf和图片的处理一记

    python是非常好用的一门语言,因为它有很多别人封装好底层逻辑的库,我们只需要用简单的函数调用或者经过并不复杂的初始化过程即可.最近要投递简历,所以在处理简历的时候遇到一个问题,就是生成了一份pdf ...

  5. DHCP介绍与实现方法

    简介:动态主机配置协议(Dynamic Host Configuration Protocol,缩写:DHCP)是 RFC 1541(已被 RFC 2131 取代)定义的标准协议,该协议允许服务器向客 ...

  6. 🎀SpringBoot启动创建系统托盘及功能

    简介 SpringBoot启动时,创建系统托盘,提供打开主程序及退出功能. 实现 启动类添加构造函数 public TjtoolApplication() { initUI(); } private ...

  7. 使用python批量爬取wallhaven.cc壁纸站壁纸

    偶然发现https://wallhaven.cc/这个壁纸站的壁纸还不错,尤其是更新比较频繁,用python写个脚本爬取 点latest,按照更新先后排序,获得新地址,发现地址是分页展示的,每一页24 ...

  8. Selenium反屏蔽处理

    Selenium自动化过程,在浏览器内会显示如下字样 当出现此内容时,有些网站就会判定是机器在进行操作,然后网站会加载防机器操作程序,如下图滑块验证 触发反机器操作的原理大概如下 解决方法 具体代码, ...

  9. app自动化的三大等待

    app自动化的三大等待与web自动化的三大等待的代码脚本一样 一.硬性等待 硬性等待无论元素是否出现,都必须等待设置的时间再继续执行后面的代码. 使用简单,但是容易浪费时间.所以一般和隐式等待或显式等 ...

  10. SkyWalking服务监控简单配置【Windows版本】

    SkyWalking是什么 skywalking是一个可观测性分析平台和应用性能管理系统专为微服务.云原生架构和基于容器(Docker.K8s.Mesos)架构而设计. 下载 官网:https://s ...