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,然后各自在重写一些自己的方法,达到线程安全。

 

posted @ 2019-04-10 17:16  yn_huang  阅读(2009)  评论(0编辑  收藏  举报