线程安全的遍历list

article/2025/8/24 12:58:57

遍历List的多种方式

在讲如何线程安全地遍历List之前,先看看通常我们遍历一个List会采用哪些方式。

方式一:

for(int i = 0; i < list.size(); i++) {System.out.println(list.get(i));
}

方式二:

Iterator iterator = list.iterator();
while(iterator.hasNext()) {System.out.println(iterator.next());
}

方式三:

for(Object item : list) {System.out.println(item);
}

方式四(Java 8):

复制代码
list.forEach(new Consumer<Object>() {@Overridepublic void accept(Object item) {System.out.println(item);}
});
复制代码

方式五(Java 8 Lambda):

list.forEach(item -> {System.out.println(item);
});

方式一的遍历方法对于RandomAccess接口的实现类(例如ArrayList)来说是一种性能很好的遍历方式。但是对于LinkedList这样的基于链表实现的List,通过list.get(i)获取元素的性能差。

方式二和方式三两种方式的本质是一样的,都是通过Iterator迭代器来实现的遍历,方式三是增强版的for循环,可以看作是方式二的简化形式。

方式四和方式五本质也是一样的,都是使用Java 8新增的forEach方法来遍历。方式五是方式四的一种简化形式,使用了Lambda表达式。

遍历List的同时操作List会发生什么?

先用非线程安全的ArrayList做个试验,用一个线程遍历List,遍历的同时另一个线程删除List中的一个元素,代码如下:

复制代码
public static void main(String[] args) {// 初始化一个list,放入5个元素final List<Integer> list = new ArrayList<>();for(int i = 0; i < 5; i++) {list.add(i);}// 线程一:通过Iterator遍历Listnew Thread(new Runnable() {@Overridepublic void run() {for(int item : list) {System.out.println("遍历元素:" + item);// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}).start();// 线程二:remove一个元素new Thread(new Runnable() {@Overridepublic void run() {// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}list.remove(4);System.out.println("list.remove(4)");}}).start();
}
复制代码

运行结果: 

遍历元素:0 
遍历元素:1 
list.remove(4) 
Exception in thread “Thread-0” java.util.ConcurrentModificationException

线程一在遍历到第二个元素时,线程二删除了一个元素,此时程序出现异常:ConcurrentModificationException。

试想如果一个老师正在点整个班级所有学生的人数(线程一遍历List),而校长(线程二)同时叫走几个学生,那么老师也肯定点不下去了。

所以我们会想到一个解决方案,那就是校长等待老师点完学生后,再叫走学生。即让线程二等待线程一的遍历完成后再进行remove元素。

使用线程安全的Vector

ArrayList是非线程安全的,Vector是线程安全的,那么把ArrayList换成Vector是不是就可以线程安全地遍历了?

将程序中的:

final List<Integer> list = new ArrayList<>();

改成:

final List<Integer> list = new Vector<>();

再运行一次试试,会发现结果和ArrayList一样会抛出ConcurrentModificationException异常。

为什么线程安全的Vector也不能线程安全地遍历呢?其实道理也很简单,看Vector源码可以发现它的很多方法都加上了synchronized来进行线程同步,例如add()、remove()、set()、get(),但是Vector内部的synchronized方法无法控制到遍历操作,所以即使是线程安全的Vector也无法做到线程安全地遍历。

如果想要线程安全地遍历Vector,需要我们去手动在遍历时给Vector加上synchronized锁,防止遍历的同时进行remove操作。相当于校长等待老师点完学生后,再叫走学生。代码如下:

复制代码
public static void main(String[] args) {// 初始化一个list,放入5个元素final List<Integer> list = new Vector<>();for(int i = 0; i < 5; i++) {list.add(i);}// 线程一:通过Iterator遍历Listnew Thread(new Runnable() {@Overridepublic void run() {// synchronized来锁住list,remove操作会在遍历完成释放锁后进行synchronized (list) {for(int item : list) {System.out.println("遍历元素:" + item);// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}}).start();// 线程二:remove一个元素new Thread(new Runnable() {@Overridepublic void run() {// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}list.remove(4);System.out.println("list.remove(4)");}}).start();
}
复制代码

运行结果:

遍历元素:0 
遍历元素:1 
遍历元素:2 
遍历元素:3 
遍历元素:4 
list.remove(4)

运行结果显示list.remove(4)的操作是等待遍历完成后再进行的。

CopyOnWriteArrayList

CopyOnWriteArrayList是java.util.concurrent包中的一个List的实现类。CopyOnWrite的意思是在写时拷贝,也就是如果需要对CopyOnWriteArrayList的内容进行改变,首先会拷贝一份新的List并且在新的List上进行修改,最后将原List的引用指向新的List。

使用CopyOnWriteArrayList可以线程安全地遍历,因为如果另外一个线程在遍历的时候修改List的话,实际上会拷贝出一个新的List上修改,而不影响当前正在被遍历的List。

相当于校长要想从班级喊走或者添加学生,需要把学生全部带到一个新的教室再进行操作,而老师则通过之前班级的快照在照片上清点学生。

复制代码
public static void main(String[] args) {// 初始化一个list,放入5个元素final List<Integer> list = new CopyOnWriteArrayList<>();for(int i = 0; i < 5; i++) {list.add(i);}// 线程一:通过Iterator遍历Listnew Thread(new Runnable() {@Overridepublic void run() {for(int item : list) {System.out.println("遍历元素:" + item);// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}).start();// 线程二:remove一个元素new Thread(new Runnable() {@Overridepublic void run() {// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}list.remove(4);System.out.println("list.remove(4)");}}).start();
}
复制代码

运行结果:

遍历元素:0 
遍历元素:1 
list.remove(4) 
遍历元素:2 
遍历元素:3 
遍历元素:4

从上面的运行结果可以看出,虽然list.remove(4)已经移除了一个元素,但是遍历的结果还是存在这个元素。由此可以看出被遍历的和remove的是两个不同的List。

线程安全的List.forEach

List.forEach方法是Java 8新增的一个方法,主要目的还是用于让List来支持Java 8的新特性:Lambda表达式。

由于forEach方法是List的一个方法,所以不同于在List外遍历List,forEach方法相当于List自身遍历的方法,所以它可以自由控制是否线程安全。

我们看线程安全的Vector的forEach方法源码:

public synchronized void forEach(Consumer<? super E> action) {...
}

可以看到Vector的forEach方法上加了synchronized来控制线程安全的遍历,也就是Vector的forEach方法可以线程安全地遍历

下面可以测试一下:

复制代码
public static void main(String[] args) {// 初始化一个list,放入5个元素final List<Integer> list = new Vector<>();for(int i = 0; i < 5; i++) {list.add(i);}// 线程一:通过Iterator遍历Listnew Thread(new Runnable() {@Overridepublic void run() {list.forEach(item -> {System.out.println("遍历元素:" + item);// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});}}).start();// 线程二:remove一个元素new Thread(new Runnable() {@Overridepublic void run() {// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}list.remove(4);System.out.println("list.remove(4)");}}).start();
}
复制代码

运行结果: 

遍历元素:0 
遍历元素:1 
遍历元素:2 
遍历元素:3 
遍历元素:4 
list.remove(4)

遍历List的多种方式

在讲如何线程安全地遍历List之前,先看看通常我们遍历一个List会采用哪些方式。

方式一:

for(int i = 0; i < list.size(); i++) {System.out.println(list.get(i));
}

方式二:

Iterator iterator = list.iterator();
while(iterator.hasNext()) {System.out.println(iterator.next());
}

方式三:

for(Object item : list) {System.out.println(item);
}

方式四(Java 8):

复制代码
list.forEach(new Consumer<Object>() {@Overridepublic void accept(Object item) {System.out.println(item);}
});
复制代码

方式五(Java 8 Lambda):

list.forEach(item -> {System.out.println(item);
});

方式一的遍历方法对于RandomAccess接口的实现类(例如ArrayList)来说是一种性能很好的遍历方式。但是对于LinkedList这样的基于链表实现的List,通过list.get(i)获取元素的性能差。

方式二和方式三两种方式的本质是一样的,都是通过Iterator迭代器来实现的遍历,方式三是增强版的for循环,可以看作是方式二的简化形式。

方式四和方式五本质也是一样的,都是使用Java 8新增的forEach方法来遍历。方式五是方式四的一种简化形式,使用了Lambda表达式。

遍历List的同时操作List会发生什么?

先用非线程安全的ArrayList做个试验,用一个线程遍历List,遍历的同时另一个线程删除List中的一个元素,代码如下:

复制代码
public static void main(String[] args) {// 初始化一个list,放入5个元素final List<Integer> list = new ArrayList<>();for(int i = 0; i < 5; i++) {list.add(i);}// 线程一:通过Iterator遍历Listnew Thread(new Runnable() {@Overridepublic void run() {for(int item : list) {System.out.println("遍历元素:" + item);// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}).start();// 线程二:remove一个元素new Thread(new Runnable() {@Overridepublic void run() {// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}list.remove(4);System.out.println("list.remove(4)");}}).start();
}
复制代码

运行结果: 

遍历元素:0 
遍历元素:1 
list.remove(4) 
Exception in thread “Thread-0” java.util.ConcurrentModificationException

线程一在遍历到第二个元素时,线程二删除了一个元素,此时程序出现异常:ConcurrentModificationException。

试想如果一个老师正在点整个班级所有学生的人数(线程一遍历List),而校长(线程二)同时叫走几个学生,那么老师也肯定点不下去了。

所以我们会想到一个解决方案,那就是校长等待老师点完学生后,再叫走学生。即让线程二等待线程一的遍历完成后再进行remove元素。

使用线程安全的Vector

ArrayList是非线程安全的,Vector是线程安全的,那么把ArrayList换成Vector是不是就可以线程安全地遍历了?

将程序中的:

final List<Integer> list = new ArrayList<>();

改成:

final List<Integer> list = new Vector<>();

再运行一次试试,会发现结果和ArrayList一样会抛出ConcurrentModificationException异常。

为什么线程安全的Vector也不能线程安全地遍历呢?其实道理也很简单,看Vector源码可以发现它的很多方法都加上了synchronized来进行线程同步,例如add()、remove()、set()、get(),但是Vector内部的synchronized方法无法控制到遍历操作,所以即使是线程安全的Vector也无法做到线程安全地遍历。

如果想要线程安全地遍历Vector,需要我们去手动在遍历时给Vector加上synchronized锁,防止遍历的同时进行remove操作。相当于校长等待老师点完学生后,再叫走学生。代码如下:

复制代码
public static void main(String[] args) {// 初始化一个list,放入5个元素final List<Integer> list = new Vector<>();for(int i = 0; i < 5; i++) {list.add(i);}// 线程一:通过Iterator遍历Listnew Thread(new Runnable() {@Overridepublic void run() {// synchronized来锁住list,remove操作会在遍历完成释放锁后进行synchronized (list) {for(int item : list) {System.out.println("遍历元素:" + item);// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}}).start();// 线程二:remove一个元素new Thread(new Runnable() {@Overridepublic void run() {// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}list.remove(4);System.out.println("list.remove(4)");}}).start();
}
复制代码

运行结果:

遍历元素:0 
遍历元素:1 
遍历元素:2 
遍历元素:3 
遍历元素:4 
list.remove(4)

运行结果显示list.remove(4)的操作是等待遍历完成后再进行的。

CopyOnWriteArrayList

CopyOnWriteArrayList是java.util.concurrent包中的一个List的实现类。CopyOnWrite的意思是在写时拷贝,也就是如果需要对CopyOnWriteArrayList的内容进行改变,首先会拷贝一份新的List并且在新的List上进行修改,最后将原List的引用指向新的List。

使用CopyOnWriteArrayList可以线程安全地遍历,因为如果另外一个线程在遍历的时候修改List的话,实际上会拷贝出一个新的List上修改,而不影响当前正在被遍历的List。

相当于校长要想从班级喊走或者添加学生,需要把学生全部带到一个新的教室再进行操作,而老师则通过之前班级的快照在照片上清点学生。

复制代码
public static void main(String[] args) {// 初始化一个list,放入5个元素final List<Integer> list = new CopyOnWriteArrayList<>();for(int i = 0; i < 5; i++) {list.add(i);}// 线程一:通过Iterator遍历Listnew Thread(new Runnable() {@Overridepublic void run() {for(int item : list) {System.out.println("遍历元素:" + item);// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}).start();// 线程二:remove一个元素new Thread(new Runnable() {@Overridepublic void run() {// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}list.remove(4);System.out.println("list.remove(4)");}}).start();
}
复制代码

运行结果:

遍历元素:0 
遍历元素:1 
list.remove(4) 
遍历元素:2 
遍历元素:3 
遍历元素:4

从上面的运行结果可以看出,虽然list.remove(4)已经移除了一个元素,但是遍历的结果还是存在这个元素。由此可以看出被遍历的和remove的是两个不同的List。

线程安全的List.forEach

List.forEach方法是Java 8新增的一个方法,主要目的还是用于让List来支持Java 8的新特性:Lambda表达式。

由于forEach方法是List的一个方法,所以不同于在List外遍历List,forEach方法相当于List自身遍历的方法,所以它可以自由控制是否线程安全。

我们看线程安全的Vector的forEach方法源码:

public synchronized void forEach(Consumer<? super E> action) {...
}

可以看到Vector的forEach方法上加了synchronized来控制线程安全的遍历,也就是Vector的forEach方法可以线程安全地遍历

下面可以测试一下:

复制代码
public static void main(String[] args) {// 初始化一个list,放入5个元素final List<Integer> list = new Vector<>();for(int i = 0; i < 5; i++) {list.add(i);}// 线程一:通过Iterator遍历Listnew Thread(new Runnable() {@Overridepublic void run() {list.forEach(item -> {System.out.println("遍历元素:" + item);// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});}}).start();// 线程二:remove一个元素new Thread(new Runnable() {@Overridepublic void run() {// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}list.remove(4);System.out.println("list.remove(4)");}}).start();
}
复制代码

运行结果: 

遍历元素:0 
遍历元素:1 
遍历元素:2 
遍历元素:3 
遍历元素:4 
list.remove(4)


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

相关文章

List的线程安全

List的线程安全 背景实验1. ArrayList2. synchronizedList3. 运行抛出异常ArrayIndexOutOfBoundsException异常原因 背景 Q&#xff1a;今天遇到一个场景&#xff0c;我们业务需要使用批量的数据进行操作&#xff0c;但是别人的接口只支持一个一个的查&#xff0c;所以需要用多…

线程安全 List 效率测试

List 常见类以及各自优缺点可自行参考 https://blog.csdn.net/weixin_39883065/article/details/111197724 本机环境 java 版本&#xff1a;1.8.0_161 window 信息&#xff1a; 测试代码 下面通过代码测试 List 线程安全类 Vector、Collections.synchronizedList(List lis…

三种线程安全的List

在单线程开发环境中&#xff0c;我们经常使用ArrayList作容器来存储我们的数据&#xff0c;但它不是线程安全的&#xff0c;在多线程环境中使用它可能会出现意想不到的结果。 多线程中的ArrayList&#xff1a; 我们可以从一段代码了解并发环境下使用ArrayList的情况&#xff…

线程安全的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;一…