我们都知道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在插入的过程中会创建新的数组,这样在数据量特别大的情况下,对内存的消耗是很大的。当然,如果是读取操作远远大于插入时,第二种方式肯定更占优势,毕竟读取操作完全不需要加锁。