遍历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
方式五(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
做个试验,用一个线程通过增强的 for
循环遍历 List
,遍历的同时另一个线程删除 List
中的一个元素,代码如下:
public static void main(String[] args) {
// 初始化一个list,放入5个元素
final List 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
正在通过迭代器遍历时,同时另外一个线程对这个 List
进行修改,就会发生异常。
使用线程安全的Vector
ArrayList
是非线程安全的,Vector
是线程安全的,那么把 ArrayList
换成 Vector
是不是就可以线程安全地遍历了?
将程序中的:
final List list = new ArrayList<>();
改成:
final List 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 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 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 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)