Java集合:CopyOnWriteArrayList与SynchronizedList
CopyOnWriteArrayList
首先提两点:
1、CopyOnWriteArrayList位于java.util.concurrent包下,可想而知,这个类是为并发而设计的
2、CopyOnWriteArrayList,顾名思义,Write的时候总是要Copy,也就是说对于CopyOnWriteArrayList,任何可变的操作(add、set、remove等等)都是伴随复制这个动作的,后面会解读CopyOnWriteArrayList的底层实现机制
四个关注点在CopyOnWriteArrayList上的答案
关注点 | 结论 |
允许空 | 是 |
允许重复数据 | 是 |
是否有序 | 有序 |
线程安全 | 安全 |
如何向CopyOnWriteArrayList中添加元素
对于CopyOnWriteArrayList来说,增加、删除、修改、插入的原理都是一样的,所以用增加元素来分析一下CopyOnWriteArrayList的底层实现机制就可以了。
先看一下 CopyOnWriteArrayList 的结构:
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; /** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array; ..... }
构造方法
/** * Creates an empty list. */ public CopyOnWriteArrayList() { setArray(new Object[0]); } /** * Sets the array. */ final void setArray(Object[] a) { array = a; }
看到,对于CopyOnWriteArrayList来说,底层就是一个Object[] array,然后实例化一个CopyOnWriteArrayList,Object array指向一个数组大小为0的数组。
添加元素
public boolean add(E e) { final ReentrantLock lock = this.lock; // 加锁 lock.lock(); try { // 获取数组 Object[] elements = getArray(); int len = elements.length; // 通过复制生成一个新的数组,长度为原来长度+1 Object[] newElements = Arrays.copyOf(elements, len + 1); // 在最后添加元素 newElements[len] = e; // 重新设置数组 setArray(newElements); return true; } finally { // 释放锁 lock.unlock(); } }
可以看出,每次添加元素,CopyOnWriteArrayList都要进行复制数组,代价十分昂贵。
读的时候是不需要加锁的,直接获取。删除和增加是需要加锁的。
有两点必须讲一下。我认为CopyOnWriteArrayList这个并发组件,其实反映的是两个十分重要的分布式理念:
(1)读写分离
我们读取CopyOnWriteArrayList的时候读取的是CopyOnWriteArrayList中的Object[] array,但是修改的时候,操作的是一个新的Object[] array,读和写操作的不是同一个对象,这就是读写分离。这种技术数据库用的非常多,在高并发下为了缓解数据库的压力,即使做了缓存也要对数据库做读写分离,读的时候使用读库,写的时候使用写库,然后读库、写库之间进行一定的同步,这样就避免同一个库上读、写的IO操作太多
(2)最终一致
对CopyOnWriteArrayList来说,线程1读取集合里面的数据,未必是最新的数据。因为线程2、线程3、线程4四个线程都修改了CopyOnWriteArrayList里面的数据,但是线程1拿到的还是最老的那个Object[] array,新添加进去的数据并没有,所以线程1读取的内容未必准确。不过这些数据虽然对于线程1是不一致的,但是对于之后的线程一定是一致的,它们拿到的Object[] array一定是三个线程都操作完毕之后的Object array[],这就是最终一致。最终一致对于分布式系统也非常重要,它通过容忍一定时间的数据不一致,提升整个分布式系统的可用性与分区容错性。当然,最终一致并不是任何场景都适用的,像火车站售票这种系统用户对于数据的实时性要求非常非常高,就必须做成强一致性的。
最后总结一点,随着CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代价将越来越昂贵,因此,CopyOnWriteArrayList适用于读操作远多于修改操作的并发场景中。
Collections.SynchronizedList
它是Collections中的一个静态内部类,它继承于Collections中的一个另外一个内部类:SynchronizedCollection,可以通过Collections.synchronizedList(List<T> list)方法获取,因为需要传入可以List的实例,因此它可以把一个非安全的List实例转换为一个安全的List。
首先我们看一下SynchronizedCollection的结构:
static class SynchronizedCollection<E> implements Collection<E>, Serializable { private static final long serialVersionUID = 3053995032091335093L; final Collection<E> c; // Backing Collection final Object mutex; // 相当于同步锁 SynchronizedCollection(Collection<E> c) { this.c = Objects.requireNonNull(c); // 锁初始化为this mutex = this; } ..... }
在它的添加和删除方法中都加入同步代码块
public boolean add(E e) { synchronized (mutex) { return c.add(e); } } public boolean remove(Object o) { synchronized (mutex) { return c.remove(o); } }
因此,在Collections类中,其实就是简单地利用了同步代码块,原来线程不安全的集合,通过继承其内部类SynchronizedCollections,然后各自在重写一些自己的方法,达到线程安全。