【Java并发】 - CopyOnWriteArrayList,CopyOnWriteArraySet原理

本文详细分析了Java并发编程中CopyOnWriteArrayList和CopyOnWriteArraySet的实现原理,包括它们的关键属性、构造方法、主要方法如add、get、set等,以及迭代器的实现。CopyOnWriteArraySet确保元素不重复,两者在读取和查询操作上表现出高效率,但在多线程写操作时会复制数组,带来额外开销,并可能导致数据的最终一致性而非实时性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

CopyOnWriteArrayList, CopyOnWriteArraySet这两个容器类是从java1.5开始加入的java.util.concurrent包的。其中与HashSet类似的CopyOnWriteArraySet内部也是直接使用的CopyOnWriteArrayList,所以本文着重介绍CopyOnWriteArrayList(简称COW)的原理。
CopyOnWriteArrayList是一个很好用的并发容器,内部还是和ArraList一样使用的数组结构来实现,所以在查询上面有着很高的效率,同时就和它的名字一样,是一个在写的时候copy,意思是在进行写操作的时候先copy一个一样的数组在这个数组上进行写操作之后随即将新的数组赋值回去。这样就使得并发环境下不会存在同时对数组写产生的问题。

源码分析


类定义

源码:
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

很常规的还是实现了List接口,同时实现了RandomAccess,Cloneable和Serializable接口表示COW支持快速随机访问,克隆和序列化。


关键属性

final transient ReentrantLock lock = new ReentrantLock();

private transient volatile Object[] array;

COW中没有什么繁杂的属性定义,其中array属性用来存放数据元素,同时使用了volatile关键字来修饰,意味着在对array的读/写(仅仅是读和写,不包含复合操作,可以参考volatile的语义)是符合原子性的。同时使用了重入锁来作为写操作的时候做锁。

构造方法

COW提供了三种构造方法,相对来说都比较常规,但是值得注意的是当使用无参的构造方法的时候是直接new一个长度为0的数组作为容器,这就意味着当我们往COW里面添加元素的时候,因为数组是定长的,所以会去创建新的数组,虽然所有的写操作都会有这个操作,但是对于初始化的时候来说直接用无参构造器来初始化是相对开销很大的。
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

final void setArray(Object[] a) {
    array = a;
}

关键方法

add方法

源码:


//末尾添加
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
		//取得原数组的拷贝并将length扩大了1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        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;
		//如果index就是在最后则直接将原数组复制过来并长度加1
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);
        else {
	 		//新构造一个数组
			//将0到index-1的元素赋值到新的数组(System.arraycopy的最后一个参数是length,所以对于下标来说是index-1)
			//将index及以后的元素赋值到新数组的index+1及以后的位置(留出了index处的元素)
            newElements = new Object[len + 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
		//设置index的值
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}
实现方式其实很简答,基本和ArrayList差不多,不同的是在写操作的时候使用了重入锁来加锁,同时写的时候是先复制一个新的数组然后在新的数组中进行写操作最后赋值给array。

这里可能有人会疑惑为什么又要加锁同时又要复制一份?直接加锁不就OK了?

那是因为COW在读的时候是没有锁的,就是最简单的读,所以这里的加锁是针对的写线程加的,防止两个线程同时写。而复制一份数组来进行写操作的目的就是使得在写操作完成后调用了setArray方法之后读操作能立马看到写的内容(这里是因为array是volatile修饰的,因为对于volatile修饰的变量的写都将happens-before于这个变量的写而不存在重排序)。

get方法

get方法就是很简答的获取到相应位置的值
public E get(int index) {
    return get(getArray(), index);
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

set方法

set方法基本思路也是加锁并复制数组然后写完后赋值回去
public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);//即使传入的值和当前位置的值相同还是调用setArray
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}
思路很清晰易懂,但是当传入的值和当前位置相同的时候还是调用了setArray方法
这里的英文作者的注释也说明白了, 这里这么做的目的是为了保证volatile的语义,也就是对于volatile变量的读总能看到之前的写的结果。因为这里调用的是set方法,set方法本身就就是一个写操作,所以这里还是调用setArray方法去保证volatile的语义。当然不调用也是可以的。
与之相对的下面的addIfAbsent方法就没有做这个操作,因为addIfAbsent方法的含义就是“如果不存在就插入”,也就是存在的话就部插入了。

addIfAbsent方法

public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
	//如果存在当前元素则返回false,不存在则调用addIfAbsent方法
    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);
			遍历公共部分如果存在则返回false
            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();
    }
}
思路很简单,但是做了稳定性的判断保证了并发的正确性。



其他的比如remove方法其实也是大同小异,这里部一一贴出来了


迭代器实现

COW的迭代器同时也是复制一个同样的数组然后在这个数组之上进行迭代的操作,正因为是复制的数组,所以不支持在迭代的时候remove,add,set等操作(在复制的数组上做了也没什么意义)。

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

static final 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;
    }

    public boolean hasPrevious() {
        return cursor > 0;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor-1;
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }

    public void set(E e) {
        throw new UnsupportedOperationException();
    }

    public void add(E e) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        Object[] elements = snapshot;
        final int size = elements.length;
        for (int i = cursor; i < size; i++) {
            @SuppressWarnings("unchecked") E e = (E) elements[i];
            action.accept(e);
        }
        cursor = size;
    }
}

实现方式也比较常规,因为是在数组上迭代,所以效率很高。

CopyOnWriteArraySet

CopyOnWriteArraySet和CopyOnWriteArrayList相比的区别是CopyOnWriteArraySet中的元素是不重复的。

实现原理


CopyOnWriteArraySet的内部结构使用的是CopyOnWriteArrayList,区别是在调用add方法的时候做了是否重复的判断,这刚好也就和CopyOnWriteArrayList的addIfAbsent方法的语义是一致的。
实际上也就是这么做的
public boolean add(E e) {
        return al.addIfAbsent(e);
}
其他如get,set,remove方法都是直接调用的CopyOnWriteArrayList的方法,这里不贴了。

小结

COW的优缺点


优点:
  • 读取和查询操作效率很高
  • 迭代效率很高
  • 多个线程同时写不会有问题

缺点:
  • 由于数组的复制而带来额外的开销。
  • 不能保证数据的实时性,只能保证最终一致性。(读的时候有线程正在写的话还是读到旧的数据)
  • 迭代的时候不支持写操作(虽然实现了ListIterator接口)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值