众所周知,ArrayList
不是线程安全的,在并发场景使用ArrayList
可能会致使add内容为null,迭代时并发修改list内容抛ConcurrentModificationException
异常等问题。java类库里面提供了如下三个轮子能够实现线程安全的List,它们是html
本文简要的分析了下它们线程安全的实现机制并对它们的读,写,迭代性能进行了对比。java
从JDK1.0开始,Vector
便存在JDK中,Vector
是一个线程安全的列表,底层采用数组实现。其线程安全的实现方式很是粗暴:Vector
大部分方法和ArrayList
都是相同的,只是加上了synchronized
关键字,这种方式严重影响效率,所以,再也不推荐使用Vector
了。JAVA官方文档中这样描述:面试
If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.数据库
若是不须要线程安全性,推荐使用ArrayList替代Vector编程
关键源码以下:后端
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized Iterator<E> iterator() {
return new Itr();
}
复制代码
能够看到Vector
经过在方法级别上加入了synchronized
关键字实现线程安全性。设计模式
由于ArrayList不是线程安全的,JDK提供了一个Collections.synchronizedList
静态方法将一个非线程安全的List(并不只限ArrayList)包装为线程安全的List。使用方式以下:数组
List list = Collections.synchronizedList(new ArrayList());
复制代码
根据文档,转换包装后的list能够实现add,remove,get等操做的线程安全性,可是对于迭代操做,Collections.synchronizedList
并无提供相关机制,因此迭代时须要对包装后的list(敲黑板,必须对包装后的list进行加锁,锁其余的不行)进行手动加锁,使用方式以下:缓存
List list = Collections.synchronizedList(new ArrayList());
//必须对list进行加锁
synchronized (list) {
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
}
复制代码
这个地方要注意两个地方:安全
synchronized
关键字修饰;synchronized (list)
,即包装后的list,使用其余对象如synchronized (new Object())
会使add
,remove
等方法与迭代方法使用的锁不一致,没法实现彻底的线程安全性。经过源码可知Collections.synchronizedList
生成了特定同步的SynchronizedCollection
,生成的集合每一个同步操做都是持有mutex
这个锁,因此再进行操做时就是线程安全的集合了。关键地方已经加了注释:
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
//ArrayList使用了SynchronizedRandomAccessList类
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
//SynchronizedRandomAccessList继承自SynchronizedList
static class SynchronizedRandomAccessList<E> extends SynchronizedList<E> implements RandomAccess {
}
//SynchronizedList对代码块进行了synchronized修饰来实现线程安全性
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
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);}
}
//迭代操做并未加锁,因此须要手动同步
public ListIterator<E> listIterator() {
return list.listIterator();
}
}
复制代码
CopyOnWriteArrayList
是java.util.concurrent
包下面的一个实现线程安全的List,顾名思义, Copy~On~Write~ArrayList在进行写操做(add,remove,set等)时会进行Copy操做,能够推测出在进行写操做时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]);
}
}
复制代码
能够看到CopyOnWriteArrayList
底层实现为Object[] array
数组。
添加元素:
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();
}
}
复制代码
能够看到每次添加元素时都会进行Arrays.copyOf
操做,代价很是昂贵。
读的时候是不须要加锁的,直接获取。删除和增长是须要加锁的。
有两点必须讲一下。我认为CopyOnWriteArrayList
这个并发组件,其实反映的是两个十分重要的分布式理念:
(1)读写分离
咱们读取CopyOnWriteArrayList
的时候读取的是CopyOnWriteArrayList
中的Object[] array
,可是修改的时候,操做的是一个新的Object[] array
,读和写操做的不是同一个对象,这就是读写分离。这种技术数据库用的很是多,在高并发下为了缓解数据库的压力,即便作了缓存也要对数据库作读写分离,读的时候使用读库,写的时候使用写库,而后读库、写库之间进行必定的同步,这样就避免同一个库上读、写的IO操做太多。
(2)最终一致
对CopyOnWriteArrayList
来讲,线程1读取集合里面的数据,未必是最新的数据。由于线程二、线程三、线程4四个线程都修改了CopyOnWriteArrayList
里面的数据,可是线程1拿到的仍是最老的那个Object[] array
,新添加进去的数据并无,因此线程1读取的内容未必准确。不过这些数据虽然对于线程1是不一致的,可是对于以后的线程必定是一致的,它们拿到的Object[] array
必定是三个线程都操做完毕以后的Object array[]
,这就是最终一致。最终一致对于分布式系统也很是重要,它经过容忍必定时间的数据不一致,提高整个分布式系统的可用性与分区容错性。固然,最终一致并非任何场景都适用的,像火车站售票这种系统用户对于数据的实时性要求很是很是高,就必须作成强一致性的。
经过前面的分析可知
Vector
对全部操做进行了synchronized
关键字修饰,性能应该比较差CopyOnWriteArrayList
在写操做时须要进行copy
操做,读性能较好,写性能较差Collections.synchronizedList
性能较均衡,可是迭代操做并未加锁,因此须要时须要额外注意下面写了个测试程序对三者的读,写,遍历进程了测试来验证下,测试机器信息以下:
操做系统:macOS High Sierra 10.13.6
CPU:2.8 GHz Intel Core i7
内存:16 GB 2133 MHz LPDDR3
复制代码
**
* 比较Vector,Collections.synchronizedList,CopyOnWriteArrayList读操做,写操做,遍历操做性能
*
* @author nauyus
* @date 2020年01月29日
*/
public class ListPerformanceTest {
/** * 并发数 */
public final static int THREAD_COUNT = 64;
/** * list大小 */
public final static int SIZE = 10000;
/** * 测试读性能 * * @throws Exception */
@Test
public void testGet() throws Exception {
List<Integer> list = initList();
List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>(list);
List<Integer> synchronizedList = Collections.synchronizedList(list);
Vector vector = new Vector(list);
int copyOnWriteArrayListTime = 0;
int synchronizedListTime = 0;
int vectorTime = 0;
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
copyOnWriteArrayListTime += executor.submit(new GetTestTask(copyOnWriteArrayList, countDownLatch)).get();
}
System.out.println("CopyOnWriteArrayList get method cost time is " + copyOnWriteArrayListTime);
for (int i = 0; i < THREAD_COUNT; i++) {
synchronizedListTime += executor.submit(new GetTestTask(synchronizedList, countDownLatch)).get();
}
System.out.println("Collections.synchronizedList get method cost time is " + synchronizedListTime);
for (int i = 0; i < THREAD_COUNT; i++) {
vectorTime += executor.submit(new GetTestTask(vector, countDownLatch)).get();
}
System.out.println("vector get method cost time is " + vectorTime);
}
private List<Integer> initList() {
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < SIZE; i++) {
list.add(new Random().nextInt(1000));
}
return list;
}
class GetTestTask implements Callable<Integer> {
List<Integer> list;
CountDownLatch countDownLatch;
GetTestTask(List<Integer> list, CountDownLatch countDownLatch) {
this.list = list;
this.countDownLatch = countDownLatch;
}
@Override
public Integer call() {
int pos = new Random().nextInt(SIZE);
long start = System.currentTimeMillis();
for (int i = 0; i < SIZE; i++) {
list.get(pos);
}
long end = System.currentTimeMillis();
countDownLatch.countDown();
return (int) (end - start);
}
}
复制代码
完整版代码能够点击阅读原文或公众号内回复文章编号010
获取
能够看到随着线程数的增长,三个类操做时间都有所增长,Vector
的遍历操做和CopyOnWriteArrayList
的写操做(图片中标红的部分)性能消耗尤为严重。出乎意料的是Vector
的读写操做和Collections.synchronizedList
比起来并无什么差异(印象中Vector
性能不好,实际性能差的只是遍历操做,看来仍是纸上得来终觉浅,绝知此事要躬行啊),仔细分析了下代码,虽然Vector
使用synchronized
修饰方法,Collections.synchronizedList
使用synchronized
修饰语句块,但实际锁住内容并无什么区别,性能类似也在情理之中。
CopyOnWriteArrayList
的写操做与Vector
的遍历操做性能消耗尤为严重,不推荐使用。
CopyOnWriteArrayList
适用于读操做远远多于写操做的场景。
Vector
读写性能能够和Collections.synchronizedList
比肩,但Collections.synchronizedList
不只能够包装ArrayList
,也能够包装其余List,扩展性和兼容性更好。
参考资料:
Java集合:CopyOnWriteArrayList与SynchronizedList
感谢阅读,若有收获,求
点赞
、求关注
让更多人看到这篇文章,本文首发于不止于技术的技术公众号Nauyus
,欢迎识别下方二维码获取更多内容,主要分享JAVA,微服务,编程语言,架构设计,思惟认知类等原创技术干货,2019年12月起开启周更模式,欢迎关注,与Nauyus一块儿学习。
这些年整理的几十套JAVA后端开发视频教程,包含微服务,分布式,Spring Boot,Spring Cloud,设计模式,缓存,JVM调优,MYSQL,大型分布式电商项目实战等多种内容,关注Nauyus当即回复【视频教程】无套路获取。
这些年整理的面试题资源汇总,包含求职指南,面试技巧,微软,华为,阿里,百度等多家企业面试题汇总。 本部分还在持续整理中,能够持续关注。当即关注Nauyus回复【面试题】无套路获取。