三种线程安全的List

三种线程安全的List在单线程开发环境中 我们经常使用 ArrayList 作容器来存储我们的数据 但它不是线程安全的 在多线程环境中使用它可能会出现意想不到的结果 多线程中的 ArrayList 我们可以从一段代码了解并发环境下使用 ArrayList 的情况 publicclassC publicstatic String args throwsInterr List Integer l Integer

在单线程开发环境中,我们经常使用ArrayList作容器来存储我们的数据,但它不是线程安全的,在多线程环境中使用它可能会出现意想不到的结果。

多线程中的ArrayList:

我们可以从一段代码了解并发环境下使用ArrayList的情况:

public class ConcurrentArrayList { 
    public static void main(String[] args) throws InterruptedException { 
    List<Integer> list = new ArrayList<>(); Runnable runnable = () -> { 
    for (int i = 0; i < 10000; i++) { 
    list.add(i); } }; for (int i = 0; i < 2; i++) { 
    new Thread(runnable).start(); } Thread.sleep(500); System.out.println(list.size()); } } 

代码中循环创建了两个线程,这两个线程都执行10000次数组的添加操作,理论上最后输出的结果应该为20000,但经过多次尝试,最后只出现了两种结果:

  1. 数组索引越界异常
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 10 at java.util.ArrayList.add(ArrayList.java:463) at ConcurrentArrayList.lambda$main$0(ConcurrentArrayList.java:14) at java.lang.Thread.run(Thread.java:748) 10007 
  1. 输出结果小于20000
16093 
// 默认初始大小 private static final int DEFAULT_CAPACITY = 10; ... // 数组size private int size; 

ArrayList的add方法:

 public boolean add(E e) { 
    //确定集合的大小是否足够,如果不够则会进行扩容 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } 

以上面错误1:ArrayIndexOutOfBoundsException: 10为例,出现错误的步骤如下:

  1. 假设某时刻Thread-0和Thread-1都执行到了elementData[size++] = e; 这步,获取的size大小都为9,此时轮到Thread-1执行
  2. Thread-1执行elementData[9] = e,空间刚刚好够用,赋值完后size变为10。接着轮到Thread-0执行
  3. 因为Thread-0已经跳过了ensureCapacityInternal(size + 1); 这步判断容量的检查步骤,因此它执行elementData[10] = e,而数组容量刚好为10!此时就出现了数组越界的错误。

另外,size++本身就是非原子性的,多个线程之间访问冲突,这时两个线程可能对同一个位置赋值,这就出现了出现size小于期望值的错误2结果。

线程安全的List

目前比较常用的构建线程安全的List有三种方法:

  1. 使用Vector容器
  2. 使用Collections的静态方法synchronizedList(List< T> list)
  3. 采用CopyOnWriteArrayList容器

1.使用Vector容器

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并没有什么区别(这里不比较扩容方式等细节)

2. Collections.synchronizedList(List< T> list)

public static <T> List<T> synchronizedList(List<T> list) { 
    return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(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);} } 

和写方法没什么区别,同样是使用了同步代码块。线程同步的实现原理非常简单!

通过上面的分析可以看出,无论是读操作还是写操作,它都会进行加锁,当线程的并发级别非常高时就会浪费掉大量的资源,因此某些情况下它并不是一个好的选择。针对这个问题,我们引出第三种线程安全容器的实现。

3. 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; 

三种方式的性能比较

1. 首先我们来看看三种方式在写操作的情况:

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 < ; 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 < ; 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 < ; i++) { 
    list.add(i); } long time2 = System.currentTimeMillis(); System.out.println("copyOnWriteArrayList: "+(time2-time1)); } } 
vector: 3202 synchronizedList: 1795 copyOnWriteArrayList: 8159 

第三种方式使用的时间远大于前两种,写操作越多,时间差就越明显。

看似出乎意料,实则意料之中,copyOnWriteArrayList每进行一次写操作都会复制一次数组,这是非常耗时的操作,因此在面临巨大的写操作量时才会差异这么大。

不过前两种方式之间为什么差异也很明显?可能因为同步代码块比同步方法效率更高?但是同步代码块是直接包含ArrayList的add方法,理论上两种同步方式应该差异不大,欢迎大佬指点。

我们再来看看三种方式在读操作的情况:

2. 我们再来看看三种方式在读操作的情况:

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 < ; 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 < ; 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 < ; i++) { 
    list.get(0); } long time2 = System.currentTimeMillis(); System.out.println("copyOnWriteArrayList: "+(time2-time1)); } } 

这一次三种方式都进行了次读操作,结果如下:

vector: 217 synchronizedList: 224 copyOnWriteArrayList: 12 

这次copyOnWriteArrayList的优势就显示出来了,它的读操作没有实现同步,因此加快了多线程的读操作。其他两种方式的差别不大。

总结

  1. 获取线程安全的List我们可以通过Vector、Collections.synchronizedList()方法和CopyOnWriteArrayList三种方式
  2. 读多写少的情况下,推荐使用CopyOnWriteArrayList方式
  3. 读少写多的情况下,推荐使用Collections.synchronizedList()的方式

参考:

  1. 并发容器(二)—线程安全的List
  2. SynchronizedList和Vector的区别
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/215853.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月18日 下午1:00
下一篇 2026年3月18日 下午1:01


相关推荐

  • MySQL增删改查_sql where case when

    MySQL增删改查_sql where case whensqlserver数据库中raiserror函数的用法server数据库中raiserror的作用就和asp.NET中的thrownewException一样,用于抛出一个异常或错误。这个错误可以被程序捕捉到。raiserror的常用格式如下:raiserror(‘错误的描述’,错误的严重级别代码,错误的标识,错误的描述中的参数的值(这个可以是多个),一些其它参数),在官方上的格式描述如下:…

    2025年6月17日
    5
  • Idea激活码最新教程2023.2.5版本,永久有效激活码,亲测可用,记得收藏

    Idea激活码最新教程2023.2.5版本,永久有效激活码,亲测可用,记得收藏Idea 激活码教程永久有效 2023 2 5 激活码教程 Windows 版永久激活 持续更新 Idea 激活码 2023 2 5 成功激活

    2025年5月27日
    5
  • pycharm注释的快捷键_pycharm注释比较多怎么办

    pycharm注释的快捷键_pycharm注释比较多怎么办用鼠标选中需要注释的代码,三次按下:shift+‘即可快速注释

    2022年8月28日
    5
  • idea2022.01.12mybatiscodehelperpro激活码(JetBrains全家桶)[通俗易懂]

    (idea2022.01.12mybatiscodehelperpro激活码)这是一篇idea技术相关文章,由全栈君为大家提供,主要知识点是关于2021JetBrains全家桶永久激活码的内容IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html4KDDGND3CI-eyJsaWN…

    2022年4月1日
    157
  • latex中参考文献怎么弄进去_论文中的参考文献怎么标注右上角

    latex中参考文献怎么弄进去_论文中的参考文献怎么标注右上角参考文献常见问题集1.请问如何将参考文献的计算器置零,然后再计数,格式大致是这样:1文12文2…1文12文2我是这样实现的:beginthebibliography99endthebibliography……beginthebibliography9999endthebibliography我的文本实在ScienticWorkplace中编辑的,建议你也使用这个软件,很…

    2025年10月14日
    6
  • 回滚 rollback

    回滚 rollback为了保证在应用程序 数据库或系统出现错误后 数据库能够被还原 以保证数据库的完整性 所以需要进行回滚 回滚 rollback 就是在事务提交之前将数据库数据恢复到事务修改之前数据库数据状态 例如 用户 A 给用户 B 转账 在数据库中就需要给 A 与 B 的账户信息进行修改 update 操作 而这两条 sql 语句必须都执行或者都不执行 例如先执行用户 B 的修改 update 语句 使用户 B 的账户金额增加了 1

    2026年3月18日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号