目前比较常用的构建线程安全的List有三种方法:
- 使用Vector容器
- 使用Collections的静态方法synchronizedList(List< T> list)
- 采用CopyOnWriteArrayList容器
Vector
Vector类实现了可扩展的对象数组,并且它是线程安全的。它和ArrayList在常用方法的实现上很相似,不同的只是它使用了同步关键词synchronized修饰方法。
Vector中的add方法:
public void add(int index, E element) {
insertElementAt(element, index);
}
...
// 使用了synchronized关键词修饰
public synchronized void insertElementAt(E obj, int index) {
modCount++;
if (index > elementCount) {
throw new ArrayIndexOutOfBoundsException(index
+ " > " + elementCount);
}
ensureCapacityHelper(elementCount + 1);
System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
elementData[index] = obj;
elementCount++;
}
Collections.synchronizedList
使用这种方法我们可以获得线程安全的List容器,它和Vector的区别在于它采用了同步代码块实现线程间的同步。通过分析源码,它的底层使用了新的容器包装原始的List。
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
其中,mutex是final修饰的一个对象:
final Object mutex;
我们可以看到,这种线程安全容器是通过同步代码块来实现的,基础的add方法任然是由ArrayList实现。
我们再来看看它的读方法:
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
和写方法没什么区别,同样是使用了同步代码块。
通过上面的分析可以看出,无论是读操作还是写操作,它都会进行加锁,当线程的并发级别非常高时就会浪费掉大量的资源,因此某些情况下它并不是一个好的选择。
CopyOnWriteArrayList
它的意思就是在写操作的时候复制数组。为了将读取的性能发挥到极致,在该类的使用过程中,读读操作和读写操作都不互斥。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 复制数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 赋值
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
从CopyOnWriteArrayList的add实现方式可以看出它是通过lock来实现线程间的同步的,这是一个标准的lock写法。
在写操作的时候,CopyOnWriteArrayList会先复制原来的数组并且在新数组上进行修改,最后再将原数组覆盖。如果写操作的过程中发生了线程切换,并且切换到读线程,因为此时数组并未发生覆盖,读操作读取的还是原数组。
换句话说,就是读操作和写操作位于不同的数组上,因此它们不会发生安全问题。
总结
获取线程安全的List我们可以通过Vector、Collections.synchronizedList()方法和CopyOnWriteArrayList三种方式
读多写少的情况下,推荐使用CopyOnWriteArrayList方式
读少写多的情况下,推荐使用Collections.synchronizedList()的方式