JUC之CopyOnWriteArrayList和CopyOnWriteArraySet

Posted fondwang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC之CopyOnWriteArrayList和CopyOnWriteArraySet相关的知识,希望对你有一定的参考价值。

一、简介

CopyOnWriteArrayList简介

  ArrayList是一种 “列表” 数据结构,其底层是通过数组来实现元素的随机访问。JDK1.5之前,如果想要在并发环境下使用 “列表”,一般有以下3种方式:

1. 使用Vector

2. 使用Collections.synchronizedList返回一个同步代理类;

3. 自己实现ArrayList的子类,并进行同步/加锁

  前两种方式都相当于加了一把“全局锁”,访问任何方法都需要首先获取锁。第3种方式,需要自己实现,复杂度较高。

  JDK1.5时,随着JUC引入了一个新的集合工具类——CopyOnWriteArrayList:

  技术图片

 

 

  大多数业务场景都是一种“读多写少”的情形,CopyOnWriteArrayList就是为适应这种场景而诞生的。

  CopyOnWriteArrayList,运用了一种“写时复制”的思想。

  通俗的理解就是当我们需要修改增/删/改)列表中的元素时,不直接进行修改,而是列表Copy,然后在新的副本上进行修改修改完成之后,在将引用原列表指向新列表。

  这样做的好处是读/写是不会冲突的,可以并发进行,读操作还是在原列表,写操作在新列表。仅仅当有多个线程同时进行写操作时,才会进行同步。

 

CopyOnWriteArraySet简介

  CopyOnWriteArraySet,是另一类适合并发环境的SET工具类。从名字上可以看出,也就是基于“写时复制” 的思想。

  事实上,CopyOnWriteArraySet内部引用了一个CopyOnWriteArrayList对象,以“组合”方式,委托CopyOnWriteArrayList对象实现了所有API功能。

二、源码分析

CopyOnWriteArraySet基本是依靠CopyOnWriteArrayList的,所以我们只分析CopyOnWriteArrayList即可

 

CopyOnWriteArrayList

 

构造器

public CopyOnWriteArrayList() { setArray(new Object[0]);}
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); //setArray方法进行初始化
}
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}

属性

final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
private static final sun.misc.Unsafe UNSAFE;
private static final long lockOffset;

核心方法

与构造器初始化相关的setArray方法

final void setArray(Object[] a) { array = a; } // 设置底层数据结构数组值
final Object[] getArray(){ return array; } //返回数据

 


查询——get方法

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

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

 

 

 

可以看到,get方法并没有加锁,直接返回了内部数组对应索引位置的值:array[index]


 

添加——add方法

// 默认添加到数组尾部
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);   // 内部array引用指向新数组
           return true;
       } finally {
           lock.unlock();   // 最后释放锁
       }
}
// 指定位置添加
public void add(int index, E element) {
    final ReentrantLock lock = this.lock;  // 获取锁对象
    lock.lock();   //加锁
    try {
        Object[] elements = getArray();  //返回旧数组
        int len = elements.length;  //旧数组长度
        if (index > len || index < 0)  // 长度合法验证
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements; 
        int numMoved = len - index;  // 
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];  // 指定长度新的空数组
            System.arraycopy(elements, 0, newElements, 0, index);  //将旧数组范围0到index-1位置上数据,赋值给新数组
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);    // 将旧数组从位置index到最后,赋值给新数组从index+1的位置开始到最后
        }
        newElements[index] = element;  //将新数组index空位置上赋值添加的元素
        setArray(newElements);   // 内部array引用指向新数组
    } finally {
        lock.unlock();  //释放锁
    }
}

 

 

 

 

 

 

  add方法首先会进行加锁,保证只有一个线程能进行修改;然后会创建一个新数组(大小为 n+1),并将原数组的值复制到新数组,新元素插入到新数组的最后;最后,将字段array指向新数组。

      技术图片

 

  上图中,ThreadB对Array的修改由于是在新数组上进行的,所以并不会对ThreadA的读操作产生影响。


 

 

删除——remove方法

三种删除方法

// 指定索引位置删除
public
E remove(int index) { final ReentrantLock lock = this.lock; //锁对象 lock.lock(); try { Object[] elements = getArray(); //原数组 int len = elements.length; //原数组长度 E oldValue = get(elements, index); //指定位置的元素 int numMoved = len - index - 1; if (numMoved == 0) setArray(Arrays.copyOf(elements, 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(); } }
// 指定对象删除
public boolean remove(Object o) { Object[] snapshot = getArray(); int index = indexOf(o, snapshot, 0, snapshot.length); // 找出对象的索引,转到对应方法 return (index < 0) ? false : remove(o, snapshot, index); } 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: { 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; } } if (index >= len) return false; if (current[index] == o) break findIndex; index = indexOf(o, current, index, len); if (index < 0) return false; } Object[] newElements = new Object[len - 1]; System.arraycopy(current, 0, newElements, 0, index); System.arraycopy(current, index + 1, newElements, index, len - index - 1); setArray(newElements); return true; } finally { lock.unlock(); } }

 

 

 

 

 

 

 

CopyOnWrite的应用场景

  CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。

CopyOnWrite的缺点

 

 

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

 

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

 

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

 

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

 

 CopyOnWriteArrayList为什么并发安全且性能比Vector好

 我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。

 

参考:https://www.cnblogs.com/myseries/p/10877420.html

    https://segmentfault.com/a/1190000016214572

以上是关于JUC之CopyOnWriteArrayList和CopyOnWriteArraySet的主要内容,如果未能解决你的问题,请参考以下文章

JUC 一 CopyOnWriteArrayList 和 CopyOnWriteArraySet

JUC中安全类集合CopyOnWriteArrayList

Java 并发JUC数据结构CopyOnWriteArrayList原理

剖析 CopyOnWriteArrayList

剖析 CopyOnWriteArrayList

CopyOnWriteArrayList 源码解读