Java进阶篇--并发容器之CopyOnWriteArrayList

目录

CopyOnWriteArrayList的简介

COW的设计思想

CopyOnWriteArrayList的实现原理

总结


CopyOnWriteArrayList的简介

CopyOnWriteArrayList是Java提供的一个线程安全的容器类。与ArrayList不同,CopyOnWriteArrayList在并发读写时可以保证线程安全,而且读写之间不会被阻塞。它适用于读多写少的场景,如系统配置信息、白名单、黑名单等配置的读取。

相比于使用synchronized关键字或ReentrantReadWriteLock对容器进行加锁,CopyOnWriteArrayList采用了一种特殊的写时复制机制,即在写操作时,先对原有数据进行复制,然后进行修改,最后将修改后的数据替换原数据。这样做的好处是,在读操作过程中,不会对读线程进行阻塞,提高了读操作的效率。因此,CopyOnWriteArrayList是一种高效的读多写少场景下的线程安全容器。

COW的设计思想

Copy-On-Write(COW)的设计思想是通过延时更新和写时复制的策略来实现数据的最终一致性和读写分离。而CopyOnWriteArrayList就是通过Copy-On-Write(COW),即写时复制的思想来通过延时更新的策略来实现数据的最终一致性,并且能够保证读线程间不阻塞。

在传统的读写锁(关于读写锁可以看这篇文章)中,当写锁被获取之后,读写线程会被阻塞,只有当写锁被释放后,读线程才能获取到锁并读取最新的数据。这样可以满足数据的实时性,但会导致读线程的阻塞。

为了解决这个问题,Copy-On-Write(COW)容器采用了写时复制的思想。当要往容器添加元素时,它不直接在当前容器上进行写操作,而是先将当前容器进行复制,创建一个新的容器,然后在新的容器上进行元素添加操作。添加完成后,再将原容器的引用指向新的容器。这样,对Copy-On-Write(COW)容器进行并发读取时,不需要加锁,因为当前容器不会被修改。

通过延时更新和写时复制的策略,Copy-On-Write(COW)容器可以实现数据的最终一致性。虽然读线程可能看不到最新的数据,但可以保证数据的一致性。这种方式避免了读写线程的阻塞,提高了读取性能。

因此,Copy-On-Write(COW)容器可以被视为一种读写分离的思想的实现,通过针对不同的数据容器进行写操作,放弃数据的实时性以达到数据最终一致性的目标。

CopyOnWriteArrayList的实现原理

CopyOnWriteArrayList 的实现原理可以概括为以下几点:

1、内部维护一个数组,并使用 volatile 修饰该数组的引用,保证可见性。

2、对于读操作(如 get 方法),直接读取数组中的数据,不需要进行任何线程安全控制,因为读操作不会修改数据。

3、对于写操作(如 add 方法),需要进行以下步骤:

  • 使用 ReentrantLock 保证同一时刻只有一个写线程进行数组的复制。
  • 获取旧数组的引用。
  • 创建一个新的数组,并将旧数组中的数据复制到新数组中。
  • 在新数组的最后添加新的元素。
  • 将旧数组的引用指向新数组。
  • 使用 unlock() 释放锁。

4、通过使用 volatile 修饰数组引用,在写操作中将新的数组分配给数组引用之后,根据 volatile 的 happens-before规则,对读线程来说,对数组引用的修改是可见的。

5、由于写操作是在新的数组中进行,而读操作仍然在旧数组中进行,从而保证了读写的并发性和一致性。

总之,CopyOnWriteArrayList通过在写操作时复制整个数组,实现了读写分离的并发策略,读操作无锁且高效,写操作使用锁保证线程安全。这种实现方式适用于读多写少的场景。

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class main {

    public static void main(String[] args) throws InterruptedException {
        // 创建一个CopyOnWriteArrayList集合
        CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();

        // 启动一个线程向集合中添加元素
        Thread addThread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                list.add(i);
                System.out.println("添加了元素:" + i);

                try {
                    Thread.sleep(100); // 等待一段时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        addThread.start();

        // 启动一个线程从集合中读取元素
        Thread readThread = new Thread(() -> {
            Iterator iterator = list.iterator();
            while (iterator.hasNext()) {
                int num = iterator.next();
                System.out.println("读取到:" + num);

                try {
                    Thread.sleep(200); // 等待一段时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        readThread.start();

        // 等待两个线程执行完毕
        addThread.join();
        readThread.join();

        System.out.println("最终集合大小:" + list.size());
    }

}

在这个示例中,我们启动了两个线程:一个线程不断向CopyOnWriteArrayList集合中添加元素,另一个线程则持续从集合中读取元素。每次添加元素时,我们都会等待一段时间,以便于读线程可以有足够的时间读取集合中的元素。

需要注意的是,虽然CopyOnWriteArrayList确保了读线程不会看到写线程进行修改时的数据,但它无法保证读操作之间的一致性。也就是说,即使两个读线程同时读取同一个集合,它们也可能看到的是不同的数据,这是因为写线程在执行完毕后,只会通知正在执行的读线程去切换到新的数组上。而对于新开启的读线程,则无法感知到数组的变更。

另外,由于CopyOnWriteArrayList使用了读写分离的并发策略,在写操作时需要复制整个数组,因此它的内存消耗较大。如果在高并发、写入量大的场景下使用,可能会导致内存占用过高甚至OOM(Out Of Memory)异常。因此,在选择并发集合时,需要根据具体的场景和需求进行选择。

总结

我们知道COW和读写锁都是通过读写分离的思想实现的,但两者还是有些不同,可以进行比较:

相同点:

  • 两者都是通过读写分离的思想实现。
  • 读线程之间互不阻塞。

不同点:

  • 在读写锁中,当写锁被获取后,读线程会等待;当读锁被获取后,写线程会等待。这样可以解决"脏读"等问题,但依然会出现读线程阻塞等待的情况。
  • 在COW中,数据的更新是延时感知的,即读线程不会等待。当写线程进行数据更新操作时,会进行复制,新旧数据同时存在。只有在写操作完成后,读线程能够感知到数据变化。因此,COW放弃了数据的实时性,但保证了数据的最终一致性。

对于COW而言,内存占用和数据一致性是需要注意的问题。由于写操作需要复制数据,会导致内存占用增加。如果内存占用比较大,写操作频繁,可能造成频繁的垃圾回收。另外,COW只能保证数据的最终一致性,无法保证数据的实时一致性。如果需要立即读取到最新的数据,不应使用COW容器。

你可能感兴趣的:(Java进阶篇,java,开发语言)