遍历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)

线程安全的遍历list相关推荐

  1. 如何线程安全地遍历List:Vector、CopyOnWriteArrayList

    原文链接:http://blog.csdn.net/xiao__gui/article/details/51050793 遍历List的多种方式 在讲如何线程安全地遍历List之前,先看看通常我们遍历 ...

  2. java 多线程遍历list_如何线程安全地遍历List:Vector、CopyOnWriteArrayList

    遍历List的多种方式 在讲如何线程安全地遍历List之前,先看看通常我们遍历一个List会采用哪些方式. 方式一: for(int i = 0; i < list.size(); i++) { ...

  3. Java线程池面试必备:核心参数、工作流、监控、调优手段

    欢迎关注方志朋的博客,回复"666"获面试宝典 1. Java的线程池 ① 合理使用线程池的好处 Java的线程池是运用场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都 ...

  4. html5 静态网页 线程,HTML5 Web Workers之网站也能多线程的实现

    Web Workers 是在HTML5中新增的,用来在web应用程序中实现后台处理的一种技术 在HTML4中,js创建的程序都是单线程的,如果花费时间比较长的话web界面就会长时间没有响应,最恶劣的情 ...

  5. (57)模拟线程切换

    一.回顾 在之前的课程中,我们学习了 EPROCESS, ETHREAD, KPCR 等重要的内核结构体,学习了存储等待线程的等待链表和调度线程的调度链表,这些知识都是为了后面学习线程切换打的基础. ...

  6. python 批量创建线程_【Python】批量创建线程

    在<[Python]线程的创建.执行.互斥.同步.销毁>(点击打开链接)中介绍了Python中线程的使用,但是里面线程的创建,使用了很原始的方式,一行代码创建一条.其实,Python里是可 ...

  7. Java核心技术 卷1 多线程----线程安全的集合(4)

    如果多线程要并发的修改一个数据结构,例如散列表,那么很容易会破坏这个数据结构.一个线程可能要开始向表中插入一个新元素.假定在调整散列表各个桶之间的链接关系的过程中,被剥夺了控制权.如果另一个线程也开始 ...

  8. CLR线程概览(一)

    托管 vs. 原生线程 托管代码在"托管线程"上执行,(托管线程)与操作系统提供的原生线程不同.原生线程是在物理机器上执行的原生代码序列:而托管线程则是在CLR虚拟机上执行的虚拟线 ...

  9. Python 线程事件 Event - Python零基础入门教程

    目录 一.Python 线程事件 Event 函数介绍 二.Python 线程事件 Event 原理 三.Python 线程事件 Event 使用 四.重点总结 五.猜你喜欢 零基础 Python 学 ...

最新文章

  1. linux7设置时间,CentOS 7 设置日期和时间
  2. Spring官方为什么建议构造器注入?
  3. 通向未来:物联网+人工智能将成为人类的进化方向
  4. innodb一页为什么要存储两行记录_InnoDB中的页合并与分裂
  5. 799. 最长连续不重复子序列 【双指针经典板子题】
  6. 干货回顾 | 泛娱乐社交 APP 出海的破与立
  7. OpenCV:判定曲线为弧线的简单方法
  8. 《你必须知道的.NET》第1章学习笔记
  9. python旋转matplotlib绘制的三维图
  10. SQL 2008 群集配置详尽攻略[3]—主节点备节点数据库安装
  11. smarty模版手册
  12. 安全加密 - 加密算法 - 摘要算法 - 秘钥交换协议 - 量子加密
  13. Python小工具之GUI倒计时提醒
  14. uniapp ios 跳转appstore
  15. Python扫码登录保存和验证cookies值——微视篇(三)
  16. 自动修改hosts文件
  17. 心理压力的测试软件,心理压力测量量表(WYB)
  18. 计算机营销专业,计算机营销专业自我评价
  19. oracle8数据库help文件增长,Oracle 10g 读书笔记
  20. Linux中的configure命令作用,linux系统configure命令详解

热门文章

  1. 什么是自定义函数?精简回答
  2. c++ 内存映射文件进程间共享数据
  3. Windows下进程间通信——共享内存
  4. NAND Flash操作技术详解
  5. STM32定时器[直接用寄存器]
  6. 扩展运算符用法实例总结
  7. oracle给一个表创建序列,oracle表中怎么创建序列
  8. CocosCreator实现道具吸引功能
  9. 《2023金融科技趋势研究报告》显示dbPaaS管理平台将成为提升数据库应用、运维效率的关键...
  10. leetcode 707 设计链表