多线程(五) -- 并发工具(二) -- J.U.C并发包(七) -- CopyOnWriteArrayList()解析及为什么要复制

1. 在学习COW时,先强调两点:

  1. JAVA中“=”操作只是将引用和某个对象关联,假如同时有一个线程将引用指向另外一个对象,一个线程获取这个引用指向的对象,那么他们之间不会发生ConcurrentModificationException,他们是在虚拟机层面阻塞的,而且速度非常快,几乎不需要CPU时间。

  2. JAVA中两个不同的引用指向同一个对象,当第一个引用指向另外一个对象时,第二个引用还将保持原来的对象。

2. 常用集合在多线程下的问题:

代码:

public class TestCopyOnWriteArrayList {
    public static void main(String[] args) {
        List list = new ArrayList();

        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

可以看到报错:并发修改异常
在这里插入图片描述

2.1 解决方案:

  1. 使用Vector类
    List list = new Vector();
  2. 使用Collections的方法:
    List list = Collections.synchronizedList(new ArrayList<>());
  3. 使用JUC中的集合类

3. CopyOnWriteArrayList:

在学习次list之前,我们先理解一个概念:CopyOnWrite:

3.1 CopyOnWrite:写入时复制

写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

简单点说就是:增删改操作会将底层数组拷贝一份,更新操作在新数组上执行,这时不影响其他线程并发读,读写分离

接下来,我们来看看CopyOnWriteArrayList的源码:

3.2 CopyOnWriteArrayList的数组对象:

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

可以看到,它是被volatile修饰的,来保证这个数组对象的可见性,关于volatile可以查看:volatile详解,这里不多解释了。

3.3 写操作

这里以add()方法为例,更多的就不多说了,源码也相对简单,这里主要说设计思想:

// JDK13,JDK8用的可重入锁
public boolean add(E e) {
	// 加锁,保证修改的同步
    synchronized (lock) {
    	// 复制数组对象
        Object[] es = getArray();
        int len = es.length;
        // 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
        es = Arrays.copyOf(es, len + 1);
        // 添加新元素
        es[len] = e;
        // 替换旧的数组,写入
        setArray(es);
        return true;
    }
}

从add方法可以看到,我们的新增操作是加了synchronized锁的,具体我也不清楚在什么时候变成了synchronized锁,但是在jdk8的时候还是用的ReentrantLock锁。

接着将数组对象复制了一份副本,用于操作(增删改),操作完成之后,再将副本的数组写入到数组对象中,由于数组对象添加了volatile关键字,所以此修改会立即被其他线程发现。

3.4 读操作:

get()方法:

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

static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}

可以看到并没有加锁,这是由于数组对象加了volatile关键字,读到的一定是最新的数据。
由于读操作没有加锁,所以CopyOnWriteArrayList集合在读取数据方面相比于Vector和Collections.synchronizedList()来说要快很多。

3.5 最重要的问题:为什么要写时复制?

这个问题困扰了我一会,明明已经加锁了,并且还加了volatile关键字,为什么还要写时复制,直接操作不行么?

关于这个问题,就不得不提到 ConcurrentModificationException。ArrayList 在 for-each 循环中删除元素时,就会抛出 ConcurrentModificationException。其表面原因是该操作破坏了 ArrayList 内部维持的一系列状态,最后在检查中不通过,导致报错。而这样设计的本质原因,还是在于保障并发读写的安全性,因为迭代期间对底层数组进行了并发修改,这样很可能会导致不可预期的错误。ArrayList 为了防止这一问题,就会在迭代期间进行检测,如果发现了有并发读写现象,就会抛出这个 ConcurrentModificationException,这是一种 fail-fast(快速失败)机制。

再回到 CopyOnWriteArrayList 的问题。CopyOnWriteArrayList 的写操作进行了加锁。如果 CopyOnWriteArrayList 只有写操作,那么这里确实只通过加锁就可以保证安全,不需要进行复制。但是 CopyOnWriteArrayList 还有读操作,而且大多数情况下,List 都是读多写少的。所以这里本质上也依然是并发读写的问题:

  1. 若没有复制,写时加锁,读时不加锁,那么就会发生并发读写问题,产生不可预期的异常,即上面说的 ConcurrentModificationException;
  2. 若没有复制,写时加锁,读时也需要加锁,这样就相当于退化为 SynchronizedList,读性能大大减弱。

而写时复制,则可以很好的处理并发读写问题,而且还保障了性能:

  1. 写时加锁,不会产生并发写的问题,保证了写操作的安全性;
  2. 实际的写操作,是在复制的新数组上进行;而同一时刻的读操作,是在原数组进行的,所以这里的读操作不会产生并发读写问题,也不需要加锁;
  3. 新数组操作完成后,将原数组替换,这里则是通过 volatile 关键字保障了新数组的线程可见性。

这样,引入写时复制的原因就说清楚了。实际上,这是 volatile、锁、写时复制三者共同作用的结果,既保证了并发读写的安全性,也保证了读的性能,三者缺一不可,可谓精妙。

一句话回答:由于读没有加锁,而volatile并不保证原子性,写的过程中的数据修改会被其他线程看到

3.6 存在的不足:

正由于上述的复制操作,导致了其存在以下问题:

  1. 存在内存占用的问题,因为每次对容器结构进行修改的时候都要对容器进行复制,这么一来我们就有了旧有对象和新入的对象,会占用两份内存。如果对象占用的内存较大,就会引发频繁的垃圾回收行为,降低性能;
  2. CopyOnWrite只能保证数据最终的一致性,不能保证数据的实时一致性
    比如:一个线程正在对容器进行修改,另一个线程正在读取容器的内容,这其实是两个容器数组。那么读线程读到的是旧数据
3.6.1 弱一致性:

迭代器弱一致性:
在这里插入图片描述
在这里插入图片描述

所以CopyOnWriteArrayList 集合比较适用于读操作远远多于写操作的场景。

3.7 相比vector和Collections.synchronizedList()的优势:

读操作的性能更加优越些,因为CopyOnWriteArrayList的读操作是通过volatile实现的,并没有加锁,而其他的方法都是用加锁实现的。
加锁意味着阻塞,阻塞意味着性能低下。

4. CopyOnWriteArraySet

思想类似,这里不多解释了。

参考:
https://patchouli-know.com/2020/05/16/copy-on-write-array-list/
https://www.jianshu.com/p/5f570d2f81a2

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值