JUC包下的CopyOnWriteArrayList是如何保证线程安全的?
在JAVA语言中,关于能解决多线程安全问题的类一般都在java.util.concurrent简称JUC包下,比如控制并发安全的ReentrantLock、ReadWriteLock的locks锁类型,还有atomic相关AtomicBoolean、AtomicInteger等,当然包下最多的还是并发安全的容器类型,例如常用的ConcurrentHashMap、 ConcurrentLinkedQueue、CopyOnWriteArrayList…今天我们就来看一下这CopyOnWriteArrayList是如何实现并发安全的,它相比于Collections 类下提供的synchronizedList有什么优缺点。
我们先来了解一下它的并发安全原理;
官方对CopyOnWriteArrayList介绍只有一句简短的话,它是一个线程安全的变体ArrayList ,其中所有可变操作(add ,set 等等)通过对底层数组的最新副本实现。看到这里,我直呼好家伙,不愧是官方,介绍语都写得那么官方,完全没有一句废话。不过从一句话中也能得出两个有用的信息,那就是变体ArrayList 和底层数组的最新副本实现。 点开它的源码是这样的,属性只有一个volatile修饰的数组,还有一个ReentrantLock锁,volatile的作用只有两个,一个是保持可见性,一个是禁止指令重排,而ReentrantLock锁的作用就是线程同步安全。结合这两者,再加上从官方介绍中提到两个信息,到这里我就能大概猜到它的线程同步安全原理了。
添加方法
容器最核心的功能是元素的写入和读取,下面我们就来看一下CopyOnWriteArrayList的添加方法是怎么保证线程安全的。 上图是CopyOnWriteArrayList 中 add 方法的实现,可以发现在添加的时候是需要加锁的,加锁的目的是防止多线程写的时候会 Copy 出 N 个副本出来。在 CopyOnWriteArrayList 里处理写操作(包括 add、remove、set 等)是先将原始的数据通过 Arrays.copyof()来生成一份新的数组,然后在新的数据对象上进行写,写完后再将原来的引用指向到当前这个数据对象,这样保证了每次写都是在新的对象上。
读取方法
然后读的时候就是在引用的当前对象上进行读(包括 get,iterator 等),不存在加锁和阻塞。 读 的 时 候 不 需 要 加 锁 , 如 果 读 的 时 候 有 线 程 正 在 向 CopyOnWriteArrayList 添加数据,读还是会读到旧的数据(在原容器中进行读)。
CopyOnWriteArrayList 中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的“=”将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用。CopyOnWriteArrayList 在读上效率很高,由于,写的时候每次都要将源数组复制到一个新组数中,所以写的效率不高。
总结一下CopyOnWriteArrayList 的特点
CopyOnWrite 容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。 1.内存占用问题。因为 CopyOnWrite 的写时复制副本的机制,所以在进行写操作的时候,内存里会同时驻扎两个数组对象的内存,旧数组对象和新写入数组对象。 针对内存占用问题,可以有如下的解决方案 1)通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是 10 进制的数字,可以考虑把它压缩成 36 进制或 64 进制。 2) 不 使 用 CopyOnWrite 容 器 , 而 使 用 其 他 的 并 发 容 器 , 如ConcurrentHashMap。 2.数据一致性问题。CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果希望写入的数据,马上就能读到的强一致性的业务需求,就不要使用 CopyOnWrite 容器。