遍历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也不能线程安全地遍历呢?其实道理也很简单,看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的更多相关文章

  1. 怎样线程安全地遍历List:Vector、CopyOnWriteArrayList

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

  2. windows 下进程与线程的遍历

    原文:http://www.cnblogs.com/Apersia/p/6579376.html 在Windows下进程与线程的遍历有好几种方法. 进程与线程的遍历可以使用<TlHelp.h&g ...

  3. 为什么ArrayList、LinkedList线程不安全,Vector线程安全

    ArrayList源码 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! ele ...

  4. 线程安全地获取插入mysql的条目的id

    在往mysql中插入条目时有时会希望能得到该插入条目的id,一种方式是再执行一个select语句条件为max(id)来获取,但这种形式在并发环境里并不是线程安全的,因为在你完成插入到再执行一个sele ...

  5. java thread 线程40个问题汇总

    http://www.codeceo.com/article/40-java-thread-problems.html 1.多线程有什么用? 一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了, ...

  6. Java并发读书笔记:线程安全与互斥同步

    目录 导致线程不安全的原因 什么是线程安全 不可变 绝对线程安全 相对线程安全 线程兼容 线程对立 互斥同步实现线程安全 synchronized内置锁 锁即对象 是否要释放锁 实现原理 啥是重进入? ...

  7. 在单线程中你最好使用ArrayList而不是Vector

    <java核心技术卷一>571页上提到Vector类的所有方法都是同步的.可以由两个线程安全地访问同一个Vector对象.显然,如果可以确定我们不会在多个线程中对这个数组进行操作的话,我们 ...

  8. 数据结构与算法(3)- C++ STL与java se中的vector

    声明:虽然本系列博客与具体的编程语言无关.但是本文作者对c++相对比较熟悉,其次是java,所以难免会有视角上的偏差.举例也大多是和这两门语言相关. 上一篇博客概念性的介绍了vector,我们有了大致 ...

  9. java容器源码分析及常见面试题笔记

      概览 容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表. List Arraylist: Object数组 ...

随机推荐

  1. iOS开发-二维码的基本使用

    二维码在生活中出现的频率越来越多了,大街小巷各个角落,它的出现更大的作用是代替功能单一的传统条形码,但是通常很多人第一次见到这个都不清楚这是干嘛用的.最初第一次见到可能就是买到的商品上有一个数字的条形 ...

  2. Sql Servicer 复习笔记(1) 存储过程分布

    第一步:创建表 declare @countInt int declare @age int ) begin ),@countInt), @age,'中国北京') ; ; ) begin ; end ...

  3. iOS -- 透明H5(webView)效果的实现

    前几天有一个完全透明的webView加载H5页面的效果的实现,就相当于是一个半透明的遮罩层,上面有一个不透明的图片,一般原生的带遮罩层的弹框会采用这种方式,如果是原生代码实现,就简单的多了,视图的叠加 ...

  4. Centos普通用户提权至ROOT

    1.利用/bin/ping的漏洞普通用户提权.(rws中的s) [root@localhost ~]# ls -l /bin/ping -rwsr-xr-x. root root 9月 /bin/pi ...

  5. 伪元素小tips

    1.伪元素的是dom看不见的,表现为行内元素.我这里说的伪元素是指::before ::after.其他的像:first-letter :visited 则属于伪类. 2.因为dom不可见,所以伪元素 ...

  6. 超链接标签a样式生效,取消下划线,文字垂直(上下)居中

    直接设置超链接标签a的属性时并不会生效, 需要将display属性改为inline-block, 即style="display:inline-block" 添加标签a时,默认是有 ...

  7. 高性能网站架构设计之缓存篇(3)- Redis 的配置

    我们说Redis是一个强大的Key-Value存储系统,在前面我们已遇到了两个问题: 1.redis server 启动后,独占进程,能不能修改为后台服务呢? 2.redis server 服务是单线 ...

  8. MSBuild 中的 PropertyGroup、ItemGroup 和 ItemMetadata

    在软件项目不断的进展中,MSBuild 脚本可能几个月都不会被修改,因为通常编译和发布的目录是不经常变化的. 但,一旦某天你需要修改了,看到那一堆 $(Something). @(Something) ...

  9. Guava - 并行编程Futures

    Guava为Java并行编程Future提供了很多有用扩展,其主要接口为ListenableFuture,并借助于Futures静态扩展. 继承至Future的ListenableFuture,允许我 ...

  10. C#版的eval,C#Light开源嵌入式脚本,unity热更新不再愁

    目前最新版本AlphaV0.06 完全的c#语法,可用于一切能运行C#的场合,wp windows xamarin mono asp.net unity3d 内嵌了int uint bool stri ...