Collections.synchronizedList 、CopyOnWriteArrayList、Vector介绍、源码浅析与性能对比【文末福利】

ArrayList线程安全问题

众所周知,ArrayList不是线程安全的,在并发场景使用ArrayList可能会导致add内容为null,迭代时并发修改list内容抛ConcurrentModificationException异常等问题。java类库里面提供了以下三个轮子可以实现线程安全的List,它们是

  • Vector
  • Collections.synchronizedList
  • CopyOnWriteArrayList

本文简要的分析了下它们线程安全的实现机制并对它们的读,写,迭代性能进行了对比。

Vector

从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关键字实现线程安全性。

Collections.synchronizedList

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

这个地方要注意两个地方:

  1. 迭代操作必须加锁,可以使用synchronized关键字修饰;
  2. 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

CopyOnWriteArrayListjava.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读取集合里面的数据,未必是最新的数据。因为线程2、线程3、线程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修饰语句块,但实际锁住内容并没有什么区别,性能相似也在情理之中。

总结


  1. CopyOnWriteArrayList的写操作与Vector的遍历操作性能消耗尤其严重,不推荐使用。


  2. CopyOnWriteArrayList适用于读操作远远多于写操作的场景。


  3. Vector读写性能可以和Collections.synchronizedList比肩,但Collections.synchronizedList不仅可以包装ArrayList,也可以包装其他List,扩展性和兼容性更好。

参考资料:

Java集合:CopyOnWriteArrayList与SynchronizedList

SynchronizedList和Vector的区别

感谢阅读,如有收获,求点赞、求关注让更多人看到这篇文章,本文首发于不止于技术的技术公众号 Nauyus ,欢迎识别下方二维码获取更多内容,主要分享JAVA,微服务,编程语言,架构设计,思维认知类等原创技术干货,2019年12月起开启周更模式,欢迎关注,与Nauyus一起学习。

福利一:后端开发视频教程

这些年整理的几十套JAVA后端开发视频教程,包含微服务,分布式,Spring Boot,Spring Cloud,设计模式,缓存,JVM调优,MYSQL,大型分布式电商项目实战等多种内容,关注Nauyus立即回复【视频教程】无套路获取。

福利二:面试题打包下载

这些年整理的面试题资源汇总,包含求职指南,面试技巧,微软,华为,阿里,百度等多家企业面试题汇总。
本部分还在持续整理中,可以持续关注。立即关注Nauyus回复【面试题】无套路获取。

原文地址:https://www.cnblogs.com/lkxsnow/p/12247524.html

时间: 2024-08-29 14:04:36

Collections.synchronizedList 、CopyOnWriteArrayList、Vector介绍、源码浅析与性能对比【文末福利】的相关文章

【转】HashMap,ArrayMap,SparseArray源码分析及性能对比

HashMap,ArrayMap,SparseArray源码分析及性能对比 jjlanbupt 关注 2016.06.03 20:19* 字数 2165 阅读 7967评论 13喜欢 43 ArrayMap及SparseArray是android的系统API,是专门为移动设备而定制的.用于在一定情况下取代HashMap而达到节省内存的目的. 一.源码分析(由于篇幅限制,源码分析部分会放在单独的文章中)二.实现原理及数据结构对比 三.性能测试对比四.总结 一.源码分析稍后会在下一篇文章中补充(都写

Volley框架源码浅析(二)

尊重原创 http://write.blog.csdn.net/postedit/25921795 在前面的一片文章Volley框架浅析(一)中我们知道在RequestQueue这个类中,有两个队列:本地队列和网络队列 /** The cache triage queue. */ private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<Request<

Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例

java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例 概要 上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具体实现类进行讲解:首先,讲解List,而List中ArrayList又最为常用.因此,本章我们讲解ArrayList.先对ArrayLis

Android应用Loaders全面详解及源码浅析

1 背景 在Android中任何耗时的操作都不能放在UI主线程中,所以耗时的操作都需要使用异步实现.同样的,在ContentProvider中也可能存在耗时操作,这时也该使用异步操作,而3.0之后最推荐的异步操作就是Loader.它可以方便我们在Activity和Fragment中异步加载数据,而不是用线程或AsyncTask,他的优点如下: 提供异步加载数据机制: 对数据源变化进行监听,实时更新数据: 在Activity配置发生变化(如横竖屏切换)时不用重复加载数据: 适用于任何Activit

Volley框架源码浅析(一)

尊重原创http://blog.csdn.net/yuanzeyao/article/details/25837897 从今天开始,我打算为大家呈现关于Volley框架的源码分析的文章,Volley框架是Google在2013年发布的,主要用于实现频繁而且粒度比较细小的Http请求,在此之前Android中进行Http请求通常是使用HttpUrlConnection和HttpClient进行,但是使用起来非常麻烦,而且效率比较地下,我想谷歌正式基于此种原因发布了Volley框架,其实出了Voll

ReactiveCocoa2 源码浅析

ReactiveCocoa2 源码浅析 标签(空格分隔): ReactiveCocoa iOS Objective-C ? 开车不需要知道离合器是怎么工作的,但如果知道离合器原理,那么车子可以开得更平稳. ReactiveCocoa 是一个重型的 FRP 框架,内容十分丰富,它使用了大量内建的 block,这使得其有强大的功能的同时,内部源码也比较复杂.本文研究的版本是2.4.4,小版本间的差别不是太大,无需担心此问题. 这里只探究其核心 RACSignal 源码及其相关部分.本文不会详细解释里

【Spark Core】任务执行机制和Task源码浅析2

引言 上一小节<任务执行机制和Task源码浅析1>介绍了Executor的注册过程. 这一小节,我将从Executor端,就接收LaunchTask消息之后Executor的执行任务过程进行介绍. 1. Executor的launchTasks函数 DriverActor提交任务,发送LaunchTask指令给CoarseGrainedExecutorBackend,接收到指令之后,让它内部的executor来发起任务,即调用空闲的executor的launchTask函数. 下面是Coars

【Spark】Stage生成和Stage源码浅析

引入 上一篇文章<DAGScheduler源码浅析>中,介绍了handleJobSubmitted函数,它作为生成finalStage的重要函数存在,这一篇文章中,我将就DAGScheduler生成Stage过程继续学习,同时介绍Stage的相关源码. Stage生成 Stage的调度是由DAGScheduler完成的.由RDD的有向无环图DAG切分出了Stage的有向无环图DAG.Stage的DAG通过最后执行的Stage为根进行广度优先遍历,遍历到最开始执行的Stage执行,如果提交的St

Java 集合系列 06 Stack详细介绍(源码解析)和使用示例

java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例 Java 集合系列 05 Vector详细介绍(源码解析)和使用示例 Java 集合系列 06 Stack详细介绍(源码解析)和使用示例 第1部分 Stack介绍 Stack简介 Stack是栈.它的特性是:先进后出(FILO, F