线程安全的List

  我们都知道ArrayList是非线程安全的,当多线程开发的时候,如果多个线程都对同一个ArrayList进行操作会报ConcurrentModificationException错误,这时我们就需要一个线程安全的List集合。
  我在开发过程中就遇到了此类问题。是在对MQ的消息处理时需要一个线程安全的List集合

Vector容器

Vector是线程安全的容器。因为它几乎在每个方法声明处都加了synchronized关键字来使容器安全

Collections的静态方法SychronizedList(List list)

SyschronizedList所属

java.util.Collections.SynchronizedList(List<T> list)

SyschronizedList能把所有的List接口的实现类转换成线程安全的List集合,比Vector具有更好的扩展性和兼容性。如果使用Collections.synchronizedList(new ArrayList())来使ArrayList变成是线程安全的话,也是几乎都是每个方法都加上synchronized关键字的,只不过它不是加在方法的声明处,而是方法的内部。SyschronizedList的构造方法如下

final List<E> list;

SynchronizedList(List<E> list) {
    super(list);
    this.list = list;
}

SyschronizedList的部分方法源码如下

public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
    synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
    synchronized (mutex) {return list.remove(index);}
}

由此可见SyschronizedList的所有方法都是带锁的(也就是说读写都是带锁的),在数据量较大,并且读多写少时,SyschronizedList的性能是很差的。因此在某些场景中他并不是最好的选择。在读取操作远大于写入操作时就可以使用下面的容器

CopyOnWrite容器

CopyOnWrite(简称:COW):即复制再写入,就是在添加元素的时候,先把原容器复制一份,再添加新的元素。
两个并发包中的并发类

java.util.concurrent.CopyOnWriteArrayList
java.util.concurrent.CopyOnWriteArraySet

CopyOnWrite集合类也就这两个,Java 1.5 开始加入

CopyOnWriteArrayList

add 方法源码

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();
    }
}

从源码中,可以看出add操作中使用了重入锁,但是此锁只针对写-写操作。为什么读写之间不用互斥,关键就在于添加值的操作并不是直接在原有数组中完成,而是使用原有数组复制一个新的数组,然后将值插入到新的数组中,最后使用新数组替换旧数组,这样插入就完成了。使用这种方式,在add的过程中旧数组没有得到修改,因此写入操作不影响读取操,另外,数组定义private transient volatile Object[] array,其中采用volatile修饰,保证内存可见性,读取线程可以马上知道这个修改。下面我们来看看读取的操作

public E get(int index) {
    return get(getArray(), index);
}
private E get(Object[] a, int index) {
    return (E) a[index];
}
final Object[] getArray() {
    return array;
}

读取操作完全没有使用任何的同步控制或者是加锁,这是因为array数组内部结构不会发生任何改变,因此读取是线程安全的。

这样做的好处是,在高并发情况下,读取元素时就不用加锁,写数据时才加锁,大大提升了读取性能。

CopyOnWriteArraySet
CopyOnWriteArraySet逻辑就更简单了,就是使用 CopyOnWriteArrayList 的 addIfAbsent 方法来去重的,添加元素的时候判断对象是否已经存在,不存在才添加进集合。

/**
 * Appends the element, if not present.
 *
 * @param e element to be added to this list, if absent
 * @return {@code true} if the element was added
 */
public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}

**这两种并发集合,只适合于读多写少的情况,如果写多读少,使用这个就没意义了,因为每次写操作都要进行集合内存复制,性能开销很大,如果集合较大,很容易造成内存溢出。

总结

根据我们的业务需要,在插入操作远远超过读取时,建议使用第一种方式,这是因为CopyOnWriteArrayList在插入的过程中会创建新的数组,这样在数据量特别大的情况下,对内存的消耗是很大的。当然,如果是读取操作远远大于插入时,第二种方式肯定更占优势,毕竟读取操作完全不需要加锁。

上一篇:比较器,堆结构,堆排序


下一篇:Linux查看物理CPU个数、核数、逻辑CPU个数