JUC源码分析-集合篇(四)CopyOnWriteArrayList

Copy-On-Write 简称 COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容 Copy 出去形成一个新的内容然后再改,这是一种延时懒惰策略。从 JDK1.5 开始 Java 并发包里提供了两个使用 CopyOnWrite 机制实现的并发容器:

  • CopyOnWriteArrayList ArrayList 线程安全的实现
  • CopyOnWriteArraySetSet 线程安全的实现

1. CopyOnWrite 容器

1.1 什么是 CopyOnWrite 容器

CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。

1.2 CopyOnWrite 应用场景

CopyOnWrite 并发容器用于读多写少的并发场景。使用 CopyOnWriteMap 需要注意两件事情:

  1. 减少扩容开销。根据实际需要,初始化 CopyOnWriteMap 的大小,避免写时 CopyOnWriteMap 扩容的开销。

  2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。

1.3 CopyOnWrite 缺点

CopyOnWrite 容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

  1. 内存占用问题。因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说 200M 左右,那么再写入 100M 数据进去,内存就会占用 300M,那么这个时候很有可能造成频繁的 Yong GC 和 Full GC。之前我们系统中使用了一个服务由于每晚使用 CopyOnWrite 机制更新大对象,造成了每晚 15 秒的 Full GC,应用响应时间也随之变长。

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是 10 进制的数字,可以考虑把它压缩成 36 进制或 64 进制。或者不使用 CopyOnWrite 容器,而使用其他的并发容器,如 ConcurrentHashMap。

  1. 数据一致性问题。CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用 CopyOnWrite 容器。

2. CopyOnWriteArrayList 实现原理

List<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
list.get(0);

在使用 CopyOnWriteArrayList 之前,我们先阅读其源码了解下它是如何实现的。以下代码是向 ArrayList 里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会 Copy 出 N 个副本出来。

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

读的时候不需要加锁,如果读的时候有多个线程正在向 ArrayList 添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的 ArrayList。

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

实现很简单,只要了解了 CopyOnWrite 机制,我们可以实现各种 CopyOnWrite 容器,并且在不同的应用场景中使用。

3. CopyOnWriteArraySet 实现原理

CopyOnWriteArraySet 底层全部使用 CopyOnWriteArrayList 实现。

public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }

// 增删改查都是调用 CopyOnWriteArrayList 的方法
public boolean add(E e) { return al.addIfAbsent(e); }
public boolean remove(Object o) { return al.remove(o); }
public boolean contains(Object o) { return al.contains(o); }

以 addIfAbsent 为例

public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
// indexOf 查找指定元素e在snapshot数组中的索引位置
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
} private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

参考:

  1. 聊聊并发-Java中的Copy-On-Write容器

每天用心记录一点点。内容也许不重要,但习惯很重要!

JUC源码分析-集合篇(四)CopyOnWriteArrayList的更多相关文章

  1. JUC源码分析-集合篇:并发类容器介绍

    JUC源码分析-集合篇:并发类容器介绍 同步类容器是 线程安全 的,如 Vector.HashTable 等容器的同步功能都是由 Collections.synchronizedMap 等工厂方法去创 ...

  2. JUC源码分析-集合篇(九)SynchronousQueue

    JUC源码分析-集合篇(九)SynchronousQueue SynchronousQueue 是一个同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然.SynchronousQu ...

  3. JUC源码分析-集合篇(三)ConcurrentLinkedQueue

    JUC源码分析-集合篇(三)ConcurrentLinkedQueue 在并发编程中,有时候需要使用线程安全的队列.如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法. ...

  4. JUC源码分析-集合篇(十)LinkedTransferQueue

    JUC源码分析-集合篇(十)LinkedTransferQueue LinkedTransferQueue(LTQ) 相比 BlockingQueue 更进一步,生产者会一直阻塞直到所添加到队列的元素 ...

  5. JUC源码分析-集合篇(八)DelayQueue

    JUC源码分析-集合篇(八)DelayQueue DelayQueue 是一个支持延时获取元素的无界阻塞队列.队列使用 PriorityQueue 来实现. 队列中的元素必须实现 Delayed 接口 ...

  6. JUC源码分析-集合篇(七)PriorityBlockingQueue

    JUC源码分析-集合篇(七)PriorityBlockingQueue PriorityBlockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现. P ...

  7. JUC源码分析-集合篇(六)LinkedBlockingQueue

    JUC源码分析-集合篇(六)LinkedBlockingQueue 1. 数据结构 LinkedBlockingQueue 和 ConcurrentLinkedQueue 一样都是由 head 节点和 ...

  8. JUC源码分析-集合篇(一)ConcurrentHashMap

    JUC源码分析-集合篇(一)ConcurrentHashMap 1. 概述 <HashMap 源码详细分析(JDK1.8)>:https://segmentfault.com/a/1190 ...

  9. JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor

    JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor.它主要用来在 ...

随机推荐

  1. 微信小程序观察者模式 observers

    const app = getApp(); const request = require('../../../utils/request.js'); Component({ options: { m ...

  2. Gym 102082B : Arithmetic Progressions

    戳这里看题目吧! 题目大意:给你一个长度为n(2<=n<=5000)的序列,没有重复的数字,序列中每个元素满足(0<=numi<=1e9).求一个最长的数列满足差分值相同(除n ...

  3. centos 7 安装mysql5.7

    1.进入到要存放安装包的位置 cd /home/lnmp 2.查看系统中是否已安装 MySQL 服务,以下提供两种方式: rpm -qa | grep mysql yum list installed ...

  4. @RequestParam 引发的编译问题

    在使用SpringMVC绑定基本类型(如String,Integer等)参数时,应通过@RequestParam注解指定具体的参数名称,否则,当源代码在非debug模式下编译后,运行时会引发Handl ...

  5. Python运维-获取当前操作系统的各种信息

    #通过Python的psutil模块,获取当前系统的各种信息(比如内存,cpu,磁盘,登录用户等),并将信息进行备份 # coding=utf-8 # 获取系统基本信息 import sys impo ...

  6. 36-python基础-python3-字典与列表的区别

    (1)不像列表,字典中的表项是不排序的.名为 spam 的列表中,第一个表项是 spam[0].但字典中没有“第一个”表项. (2)确定两个列表是否相同时,表项的顺序必须相同,但在字典确定两个字典是否 ...

  7. arcpy-栅格转其他格式

    import arcpy in_format=arcpy.GetParameterAsText(0) out_format=arcpy.GetParameterAsText(1) out_folder ...

  8. 后端大佬给我配置的deploy serves文件以便学习

    serves apiVersion: v1 kind: Service metadata: labels: com.wise2c.service: ui-ll-2 com.wise2c.stack: ...

  9. element UI datepicker组件限制可选日期范围

    长话短说,简单粗暴上代码了,在element中的datepicker,可以自由选择日期,如下: 然后我们根据element 官网的文档,给datepicker组件动态改变 picker-options ...

  10. 2018-8-10-win10-uwp-ApplicationView

    title author date CreateTime categories win10 uwp ApplicationView lindexi 2018-08-10 19:16:53 +0800 ...