三种线程安全的List

article/2025/8/24 12:54:45

在单线程开发环境中,我们经常使用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: 10at 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

虽然仍有可能得到20000的结果,但概率非常低。我们要从ArrayList的源码中去分析为什么会出现这种结果。
ArrayList数组默认初始化大小:

// 默认初始大小
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容器

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

2. 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);}
}

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

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

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 < 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方法,理论上两种同步方式应该差异不大,欢迎大佬指点。

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

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 < 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的优势就显示出来了,它的读操作没有实现同步,因此加快了多线程的读操作。其他两种方式的差别不大。

总结

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

参考:

  1. 并发容器(二)—线程安全的List
  2. SynchronizedList和Vector的区别

http://chatgpt.dhexx.cn/article/tMbMVQ08.shtml

相关文章

线程安全的List

线程安全的List Vector类的架构基本属性构造方法基本方法 SynchronizedList和SynchronizedRandomAccessListCollections.synchronizedList构造方法具体方法具体使用 CopyOnWriteArrayList(**)简介结构成员变量常见方法add (***)removeget CopyOnWriteArrayList总结 总结 在我们…

Git - 拉取远程分支并创建本地分支

一、查看远程分支 使用如下git命令查看所有远程分支 git branch -r 查看远程和本地所有分支 git branch -a 查看本地分支 git branch 在输出结果中&#xff0c;前面带* 的是当前分支 二、拉取远程分支并创建本地分支 方法一 使用如下命令 git checkout -b 本地分支名…

Git获取远程分支文件并创建自己的远程分支

Git获取远程分支文件并创建自己的远程分支 1、前期准备软件&#xff1a;git-bash 2、获取远程分支文件过程 创建一个空的文件夹&#xff0c;选择文件夹&#xff0c;并右击&#xff0c;选择该选项打开命令行。 复制需要拉取的远程仓库地址。 &#xff08;2&#xff09;把maste…

git基于远程分支创建本地分支

git基于远程分支创建本地分支 1.首先 git branch -a 查看所有的分支 2.使用 git checkout -b 本地分支名 远程分支名 https://blog.csdn.net/north1989/article/details/116299912?utm_mediumdistribute.pc_relevant.none-task-blog-baidujs_baidulandingword-0&spm1001.…

git 创建本地分支及远程分支并且关联分支

git命令在创建本地分支及远程分支并且关联远程分支 为了便于版本的维护及管理将会不断的在master分支上创建出新的分支 大致分为: 首先切换到在要开的分支上――――>创建本地分支――――>创建远程分支――――>切换到本地分支――――>将本地分支与远程分支关联…

git 创建远程分支,并提交代码到该分支的操作

1. 首先&#xff0c;在本地创建这个分支 使用命令 git checkout -b 分支名 &#xff08;表示创建这个分支&#xff0c;并且切换到该分支&#xff09; 2. 创建远程分支 使用命令 git push --set-upstream origin 分支名 &#xff08;表示将分支推送到远程仓库&#xff09; 3. …

git创建远程分支并关联本地分支

场景一&#xff1a; 本地、远程都没有分支 "v1.0.0" 1. 先查看确认一下&#xff0c;命令&#xff1a; git branch -a 2. 创建本地分支&#xff0c;命令&#xff1a; git checkout -b v1.0.0 3. 创建远程分支&#xff0c;并且本地分支关联远程分支&#xff0c;命令…

Git 创建远程分支并提交代码到远程分支

1、可以通过git branch -r 命令查看远端库的分支情况 2、从已有的分支创建新的分支(如从master分支),创建一个dev分支 但此时并没有在远程仓库上创建分支 如图所示 还是只有一个master分支 3、建立本地到远端仓库的链接 --这样代码才能提交上去 使用命令行 git push --set-…

Git创建远程分支并提交代码到远程分支

1、可以通过git branch -r 命令查看远端库的分支情况 动图演示&#xff08;选择项目右键选择 Git Bash Here&#xff0c;然后输入命令git branch -r&#xff09;&#xff1a; 2、从已有的分支创建新的分支(如从master分支),创建一个dev分支 但此时并没有在远程仓库上创建分支 如…

机器学习知识经验分享之三:基于卷积神经网络的经典目标检测算法

文章目录 前言一、一阶段目标检测算法1.YOLO系列算法2.SSD检测算法3. RetinaNet检测算法 二、两阶段目标检测算法1.Faster R-CNN检测算法2.Mask R-CNN检测算法3.Cascade R-CNN检测算法 总结 前言 本系列文章将对机器学习知识进行分享总结。便于大家从理论层面了解人工智能基础…

轻量型目标检测算法一次看个够

序言 不知道大家有没有发现&#xff0c;近两年目标检测算法发展非常的快&#xff0c;恍惚一看&#xff0c;单阶段算法几乎统一了目标检测&#xff0c;各种高性能的目标检测算法层出不穷&#xff0c;印象中是在YOLOv4出来后&#xff0c;基于YOLO的改进变得一发不可收拾&#xf…

万字长文概述单目3D目标检测算法

一&#xff0c;理论基础-相机与图像 相机将三维世界中的坐标点&#xff08;单位为米&#xff09;映射到二维图像平面&#xff08;单位为像素&#xff09;的过程能够用一个几何模型进行描述&#xff0c;这个模型有很多种&#xff0c;其中最简单的称为针孔相机模型。相机的成像过…

yolov5 目标检测算法

简介&#xff1a; 目标检测在生活中应用领域非常广泛&#xff0c;列如&#xff1a;道路违规抓拍、未戴口罩识别、工地未佩戴安全帽抓拍、厨房出现老鼠检测。 还可以用在游戏辅助外挂。以枪战为例&#xff0c;在游戏过程中时刻检测有没有人头出现。当检测到目标人头&#xff0c;…

【快速入门】YOLOv5目标检测算法

文章目录 一、YOLOv5简介二、网络结构1、Input2、Backbone3、Neck4、Head 三、改进方法1、自适应锚框计算2、自适应灰度填充 四、性能表现五、YOLOv5入门实战 一、YOLOv5简介 YOLOv5是一个在COCO数据集上预训练的物体检测架构和模型系列&#xff0c;它代表了Ultralytics对未来…

目标检测算法汇集介绍

目标检测算法 目标检测概念 目标检测这里阐述两个应用场景&#xff0c;1 为物体位置检测&#xff0c;2 为物体关键点检测。 1 物体位置检测 相比与图片分类&#xff0c;目标检测算法结果要求不仅识别出图片中的物理类别并且输出物体的位置参数。 物体的位置通过bounding bo…

YOLOv3目标检测算法——通俗易懂的解析

目录 YOLOv3目标检测算法前沿一.YOLOv3二.损失函数 YOLOv3目标检测算法 前沿 前两篇文章我们讲了下关于YOLOv1和YOLOv2的原理&#xff0c;有不懂的小伙伴可以回到前面再看看&#xff1a; YOLOv1目标检测算法——通俗易懂的解析YOLOv2目标检测算法——通俗易懂的解析 作者出于…

单阶段目标检测算法之YOLOv1详解

官方网站C语言版本:https://pjreddie.com/darknet/yolov1/ tensorflow版本的代码下载&#xff1a; https://github.com/hizhangp/yolo_tensorflow 论文&#xff1a; http://arxiv.org/abs/1506.02640 目录 一、YOLO介绍 二、YOLOv1的结构 三、YOLOV1原理 &#xff08;一…

yolo-目标检测算法简介

一 简单概念 机器视觉的四大任务 分类-Classification&#xff1a;解决“是什么&#xff1f;”的问题&#xff0c;即给定一张图片或一段视频判断里面包含什么类别的目标. 检测-Detection&#xff1a;解决“是什么&#xff1f;在哪里&#xff1f;”的问题&#xff0c;即定位出…

目标检测算法发展历程

这里的图片是从b站同济子豪兄的【精读AI论文】 R-CNN深度学习目标检测算法 处截图的&#xff0c; 在这算是个记录吧&#xff0c;同时立个小小的flag&#xff0c;尽量在硕一结束前都搞明白是干啥的吧&#xff08;希望能坚持住 &#xff1a;&#xff09; 在这也小推一下子豪兄&a…

YOLOv5目标检测算法——通俗易懂的解析

目录 YOLOv5目标检测算法前沿一.网络结构1.1.Backbone1.2.Neck1.3.Head二.数据增强2.1.Mosaic2.2.Copy paste2.3.Random affine2.4.Mixup2.5.Albumentation2.6.Augment HSV2.7.Random horizontal flip 三.训练策略3.1.Multi-scale training3.2.Auto anchor3.3.Warmup and Cosin…