Java多线程:CopyOnWriteArrayList 实现原理

Posted 杨 戬

tags:

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

文章目录

CopyOnWriteArrayList

CopyOnWriteArrayList 概念

CopyOnWriteArrayList是Java并发包里提供的并发类,简单来说它就是一个线程安全且读操作无锁的ArrayList。正如其名字一样,在写操作时会复制一份新的List,在新的List上完成写操作,然后再将原引用指向新的List。这样就保证了写操作的线程安全。

CopyOnWriteArrayList允许线程并发访问读操作,这个时候是没有加锁限制的,性能较高。而写操作的时候,则首先将容器复制一份,然后在新的副本上执行写操作,这个时候写操作是上锁的。结束之后再将原容器的引用指向新容器。注意,在上锁执行写操作的过程中,如果有需要读操作,会作用在原容器上。因此上锁的写操作不会影响到并发访问的读操作。

CopyOnWriteArrayList基于fail-safe机制,不会抛出CurrentModifyException;

CopyOnWriteArrayList 原理

基于COW机制

Copy-On-Write简称COW。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。

所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList就是基于此机制研发出来的,从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。

CopyOnWriteArrayList 源码分析

首先看下CopyOnWriteArrayList的成员变量:

  • 使用重入锁ReentrantLock保证写操作互斥
  • 使用volatile修饰的Object数组
// 重入锁保写操作互斥 
final transient ReentrantLock lock = new ReentrantLock(); 
// volatile保证读可见性
private transient volatile Object[] array;

CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为了避免Copy出N个副本出来,导致并发写。

例如下面add函数的源码:

public boolean add(E e) 
    final ReentrantLock lock = this.lock;
    lock.lock();// 加锁
    try 
        Object[] elements = getArray();// 读取原数组
        int len = elements.length;
        // 构建一个长度为len+1的新数组,然后拷贝旧数据的数据到新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 把新加的数据赋值到最后一位
        newElements[len] = e;
        // 替换旧的数组
        setArray(newElements);
        return true;
     finally 
        lock.unlock();
    

先获得锁,然后拷贝元素组并将新元素加入(添加的元素可以是null),再替换掉原来的数组。我们会发现这种实现方式非常不适合频繁修改的操作。CopyOnWriteArrayList的删除和修改的操作的原理也是类似的,代码如下:

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

CopyOnWriteArrayList中的读方法是没有加锁的。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,当然,这里读到的数据可能不是最新的。因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的,并非强一致性。

例如下面get函数的源码:

// 直接获取index对应的元素 
public E get(int index) 
    return get(getArray(), index);
 

// getarray,返回数组 
final Object[] getArray() 
    return array;


// 返回对应位置元素
private E get(Object[] a, int index) 
    return (E) a[index];

从以上的增删改查中我们可以发现,增删改都需要获得锁,并且锁只有一把,而读操作不需要获得锁,支持并发。

问题思考

为什么增删改中都需要创建一个新的数组,操作完成之后再赋给原来的引用?

这是为了保证get的时候都能获取到元素,如果在增删改过程直接修改原来的数组,可能会造成执行读操作获取不到数据。

读操作的时候进行修改,会影响到正在遍历的操作吗?

不会,CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组。而其他线程对其修改后会生成一个新的数组,所以并不影响之前数组的遍历。

在遍历传统的List时,若中途有别的线程对其进行修改,则会抛出ConcurrentModificationException异常。

而CopyOnWriteArrayList由于其"读写分离"的思想(fail-safe机制),遍历和修改操作分别作用在不同的List容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了。

CopyOnWriteArrayList 优缺点

优点:

  • 读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。

缺点:

  • 一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC。
  • 二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

CopyOnWriteArrayLis使用场景

整体来说CopyOnWriteArrayList是另类的线程安全的实现,但并一定是高效的,适合用在读取和遍历多的场景下,并不适合写并发高的场景,因为数组的拷贝也是非常耗时的,尤其是数据量大的情况下

以上是关于Java多线程:CopyOnWriteArrayList 实现原理的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程 4.线程池

什么是JAVA的多线程?

Java多线程 1.认识Java线程

Java多线程 5.栅栏

java 如何实现多线程

java中啥叫做线程?啥叫多线程?多线程的特点是啥