并发集合-CopyOnWrite系列

CopyOnWriteArrayList

CopyOnWriteArrayList 是 Java 并发包 (java.util.concurrent) 中提供的一个线程安全的 ArrayList 实现,它采用了"写时复制"(Copy-On-Write, COW)技术来实现线程安全。

核心特点

  1. 线程安全:无需额外同步即可在多线程环境下安全使用
  2. 写时复制:任何修改操作(add, set, remove等)都会创建底层数组的新副本
  3. 弱一致性:为了尽可能保证并发性能(读写不阻塞),不是时时刻刻都一致,但最终是一致的
  4. 读操作无锁:读取操作不需要加锁,性能很高

类结构

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  
  	final transient ReentrantLock lock = new ReentrantLock();
  
  	private transient volatile Object[] array;
}
  • 实现了 RandomAccess 接口,说明支持随机访问,也说明数据结构是数组了(同 ArrayList
  • 实现了 List 接口,具有 add()remove()contains()get() 等方法(同 ArrayList
  • 有个 ReenTrantLock 类型的lock 属性,用来保证写写互斥的

工作原理

add() 添加元素

  • 添加元素是写操作,所以要获取锁才能进行
  • 核心就是复制一个新数组出来,然后添加元素到这个新数组,最后用这个新数组替换原来的数组
  • 只要是写操作的方法都要先获取锁,比如 clear()add(index, element)
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();
    }
}

get() 获取元素

获取元素更简单了,直接通过下标获取元素,完全不做并发安全的操作

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

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

弱一致性

读不加锁,写加锁,读写可以并行,尽可能提高并发性能,虽然并发是高了,但是也带来一个问题:

线程A 线程B
get(0) remove(0)
setArray()
get(0)

两次读不一致,数据库的读已提交隔离级别造成的不可重复读问题
虽然表现比较一致,但是要明白 CopyOnWriteArrayList 是有意为之,故意这么做的!

迭代器弱一致性(也是副本)

public static void main(String[] args) throws InterruptedException {
    CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<Integer>(){{
        add(1);
        add(2);
        add(3);
    }};
		
  	// 迭代器也是副本,下面 t1 线程删除了第一个元素,但是不影响副本,所以主线程里第一个元素还是1
    Iterator<Integer> iterator = list.iterator();

    Thread t1 = new Thread(() -> list.remove(0)); // 删除第一个元素
    t1.join(); // 让 t1 执行完再接着执行主线程

    while (iterator.hasNext()){
        System.out.println(iterator.next());
    }
}

不要觉得弱一致性不好

  • 数据库的【读已提交】这个隔离级别也不是就不好,并发和一致性天生就是矛盾的,就看如何取舍
  • 相比【读已提交】这个隔离界别,用【MVCC】类比应该更形象

适用场景

  • 读多写少:适合读取操作远多于修改操作的场景
  • 集合较小:因为每次修改都要复制整个数组,大集合性能较差

优缺点

优点

  • 读取性能极高(无锁)
  • 迭代安全,不会抛出并发修改异常
  • 实现简单,易于理解

缺点

  • 写操作性能较差(需要复制整个数组)
  • 内存占用较大(写操作时会有两个数组存在)
  • 数据实时性不强,读取操作可能获取不到最新数据

与同步List比较

相比于使用Collections.synchronizedList()包装的ArrayList:

  • CopyOnWriteArrayList在读多写少场景下性能更好
  • 同步List在迭代时需要外部同步,否则可能抛出异常
  • 同步List的读写都是同步的,而COW只有写操作需要同步

CopyOnWriteArrayList是Java并发编程中一个非常有用的工具类,特别适合读多写少的并发场景。

CopyOnWriteArraySet

内部使用 CopyOnWriteArrayList 实现的,在添加元素时先判断是否有重复元素,没有才添加

public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {

    private final CopyOnWriteArrayList<E> al;
  	
		public boolean add(E e) {
        return al.addIfAbsent(e);
    }
}
posted @ 2023-05-24 16:56  CyrusHuang  阅读(54)  评论(0)    收藏  举报