线程安全的List
目前比较常用的构建线程安全的List有三种方法:
- 使用Vector容器
- 使用Collections的静态方法synchronizedList(List< T> list)
- 采用CopyOnWriteArrayList容器
使用Vector容器
Vector类实现了可扩展的对象数组,并且它是线程安全的。它和ArrayList在常用方法的实现上很相似,不同的只是它采用了同步关键词synchronized修饰方法。
ArrayList中的add方法:
public void add(int index, E element) {rangeCheckForAdd(index);ensureCapacityInternal(size + 1); // Increments modCount!!System.arraycopy(elementData, index, elementData, index + 1,size - index);elementData[index] = element;size++;}
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++;}
可以看出,Vector在通用方法的实现上ArrayList并没有什么区别(这里不比较扩容方式等细节)
Collections.synchronizedList(List< T> list)
使用这种方法我们可以获得线程安全的List容器,它和Vector的区别在于它采用了同步代码块实现线程间的同步。通过分析源码,它的底层使用了新的容器包装原始的List。
下图是新容器的继承关系图:
synchronizedList方法:
public static <T> List<T> synchronizedList(List<T> list) {return (list instanceof RandomAccess ?new SynchronizedRandomAccessList<>(list) :new SynchronizedList<>(list));}
因为ArrayList实现了RandomAccess接口,因此该方法返回一个SynchronizedRandomAccessList实例。
该类的add实现:
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写法。那么它是怎么做到读写互斥的呢?
// 复制数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 赋值
newElements[len] = e;
真实实现读写互斥的细节就在这两行代码上。在面临写操作的时候,CopyOnWriteArrayList会先复制原来的数组并且在新数组上进行修改,最后再将原数组覆盖。如果写操作的过程中发生了线程切换,并且切换到读线程,因为此时数组并未发生覆盖,读操作读取的还是原数组。
换句话说,就是读操作和写操作位于不同的数组上,因此它们不会发生安全问题。
另外,数组定义private transient volatile Object[] array,其中采用volatile修饰,保证内存可见性,读取线程可以马上知道这个修改。
private transient volatile Object[] array;
三种方式的性能比较
- 首先我们来看看三种方式在写操作的情况:
public class ConcurrentList {public static void main(String[] args) {testVector();testSynchronizedList();testCopyOnWriteArrayList();}public static void testVector(){Vector vector = new Vector();long time1 = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {vector.add(i);}long time2 = System.currentTimeMillis();System.out.println("vector: "+(time2-time1));}public static void testSynchronizedList(){List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());long time1 = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {list.add(i);}long time2 = System.currentTimeMillis();System.out.println("synchronizedList: "+(time2-time1));}public static void testCopyOnWriteArrayList(){CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();long time1 = System.currentTimeMillis();for (int i = 0; i < 100000; i++) {list.add(i);}long time2 = System.currentTimeMillis();System.out.println("copyOnWriteArrayList: "+(time2-time1));}
}
在代码中我让Vector和SynchronizedList两种实现方式进行写操作10000000次,而CopyOnWriteArrayList仅仅只有100000次,与前两种方式少了100倍!
而结果却出乎意料:
vector: 3202
synchronizedList: 1795
copyOnWriteArrayList: 8159
第三种方式使用的时间远大于前两种,写操作越多,时间差就越明显。
看似出乎意料,实则意料之中,copyOnWriteArrayList每进行一次写操作都会复制一次数组,这是非常耗时的操作,因此在面临巨大的写操作量时才会差异这么大。
不过前两种方式之间为什么差异也很明显?可能因为同步代码块比同步方法效率更高?但是同步代码块是直接包含ArrayList的add方法,理论上两种同步方式应该差异不大,欢迎大佬指点。
我们再来看看三种方式在读操作的情况:
- 我们再来看看三种方式在读操作的情况:
public class ConcurrentList {public static void main(String[] args) {testVector();testSynchronizedList();testCopyOnWriteArrayList();}public static void testVector(){Vector<Integer> vector = new Vector<>();vector.add(0);long time1 = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {vector.get(0);}long time2 = System.currentTimeMillis();System.out.println("vector: "+(time2-time1));}public static void testSynchronizedList(){List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());list.add(0);long time1 = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {list.get(0);}long time2 = System.currentTimeMillis();System.out.println("synchronizedList: "+(time2-time1));}public static void testCopyOnWriteArrayList(){CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();list.add(0);long time1 = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {list.get(0);}long time2 = System.currentTimeMillis();System.out.println("copyOnWriteArrayList: "+(time2-time1));}
}
这一次三种方式都进行了10000000次读操作,结果如下:
vector: 217
synchronizedList: 224
copyOnWriteArrayList: 12
这次copyOnWriteArrayList的优势就显示出来了,它的读操作没有实现同步,因此加快了多线程的读操作。其他两种方式的差别不大。
总结
获取线程安全的List我们可以通过Vector、Collections.synchronizedList()方法和CopyOnWriteArrayList三种方式
读多写少的情况下,推荐使用CopyOnWriteArrayList方式
读少写多的情况下,推荐使用Collections.synchronizedList()的方式