怎样线程安全地遍历List:Vector、CopyOnWriteArrayList
遍历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>() {
@Override
public 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遍历List
new Thread(new Runnable() {
@Override
public 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() {
@Override
public 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也不能线程安全地遍历呢?事实上道理也非常easy,看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遍历List
new Thread(new Runnable() {
@Override
public 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() {
@Override
public 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遍历List
new Thread(new Runnable() {
@Override
public 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() {
@Override
public 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遍历List
new Thread(new Runnable() {
@Override
public 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() {
@Override
public 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://xxgblog.com/2016/04/02/traverse-list-thread-safe/
怎样线程安全地遍历List:Vector、CopyOnWriteArrayList的更多相关文章
- 如何线程安全地遍历List:Vector、CopyOnWriteArrayList
遍历List的多种方式 在讲如何线程安全地遍历List之前,先看看通常我们遍历一个List会采用哪些方式. 方式一: for(int i = 0; i < list.size(); i++) { ...
- windows 下进程与线程的遍历
原文:http://www.cnblogs.com/Apersia/p/6579376.html 在Windows下进程与线程的遍历有好几种方法. 进程与线程的遍历可以使用<TlHelp.h&g ...
- 为什么ArrayList、LinkedList线程不安全,Vector线程安全
ArrayList源码 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! ele ...
- 线程安全地获取插入mysql的条目的id
在往mysql中插入条目时有时会希望能得到该插入条目的id,一种方式是再执行一个select语句条件为max(id)来获取,但这种形式在并发环境里并不是线程安全的,因为在你完成插入到再执行一个sele ...
- java thread 线程40个问题汇总
http://www.codeceo.com/article/40-java-thread-problems.html 1.多线程有什么用? 一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了, ...
- Java并发读书笔记:线程安全与互斥同步
目录 导致线程不安全的原因 什么是线程安全 不可变 绝对线程安全 相对线程安全 线程兼容 线程对立 互斥同步实现线程安全 synchronized内置锁 锁即对象 是否要释放锁 实现原理 啥是重进入? ...
- 在单线程中你最好使用ArrayList而不是Vector
<java核心技术卷一>571页上提到Vector类的所有方法都是同步的.可以由两个线程安全地访问同一个Vector对象.显然,如果可以确定我们不会在多个线程中对这个数组进行操作的话,我们 ...
- 数据结构与算法(3)- C++ STL与java se中的vector
声明:虽然本系列博客与具体的编程语言无关.但是本文作者对c++相对比较熟悉,其次是java,所以难免会有视角上的偏差.举例也大多是和这两门语言相关. 上一篇博客概念性的介绍了vector,我们有了大致 ...
- java容器源码分析及常见面试题笔记
概览 容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表. List Arraylist: Object数组 ...
随机推荐
- 谈HTTPS中间人攻击与证书校验(一)
一.前言 随着安全的普及,https通信应用越发广泛,但是由于对https不熟悉导致开发人员频繁错误的使用https,例如最常见的是未校验https证书从而导致“中间人攻击”,并且由于修复方案也一直是 ...
- bzoj 1415 无环期望
#include <cstdio> #include <vector> #include <queue> #include <algorithm> #d ...
- php-streams扩展学习
一. streams是干嘛的: 用于统一文件.网络.数据压缩等类文件操作方式,并为这些类文件操作提供一组通用的函数接口. 二. stream是具有流式行为的资源对象,这个对象有一个包装类 例如: pr ...
- Rails -- 自动清除日志
在开发模式中,开发环境日志会越来越大,所以需要设置自动清理,省内存. 在 config/initializers中新建一个文件 clear_blog.rb 编写如下代码 if Rails.env.de ...
- ACM -- 算法小结(四)KMP(POJ3461)
KMP -- POJ3461解题报告 问题描述:给出字符串P和字符串T,问字符串P在字符串T中出现的次数 Sample Input 3 BAPC BAPC AZA AZAZAZA VERDI ...
- svm算法介绍
在一个理想的分类当中,我们想要用一个超平面来将正类样本和负类样本划分开来.这个超平面的方程为 $\mathbf{w}^T\mathbf{x}+b=0$ 我们希望这个超平面能够使得划分更加的鲁棒,在图形 ...
- Hive集合数据类型
Hive的列除了支持基本的数据类型外,还支持使用Struct.Map和Array三种集合数据类型. 假设某表有如下一行,我们用JSON格式来表示其数据结构.在Hive下访问的格式为 { "n ...
- UVA 11205 The broken pedometer(子集枚举)
B - The broken pedometer Time Limit:3000MS Memory Limit:0KB 64bit IO Format:%lld & %llu ...
- 虚拟机安装Linux过程和踩坑
由于想学习node,服务器端大都使用Linux系统,所以就想着在笔记本上弄个虚拟机,装上Linux,使用xshell在window上操作也方便,也借此来熟悉一下Linux,接下来就解释下安装的步骤和遇 ...
- C#各种结束进程的方法详细介绍
Process类的CloseMainWindow, Kill, Close Process.CloseMainWindow是GUI程序的最友好结束方式,从名字上就可以看出来它是通过结束主窗体,相当于用 ...