在讲如何线程安全地遍历 List
以前,先看看遍历一个 List
一般会采用哪些方式。java
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); }
list.forEach(new Consumer<Object>() { @Override public void accept(Object item) { System.out.println(item); } });
list.forEach(item -> { System.out.println(item); });
方式一的遍历方法对于 RandomAccess
接口的实现类(例如 ArrayList
)来讲是一种性能很好的遍历方式。可是对于 LinkedList
这样的基于链表实现的 List
,经过 list.get(i)
获取元素的性能差。安全
方式二和方式三两种方式的本质是同样的,都是经过 Iterator
迭代器来实现的遍历,方式三是加强版的 for
循环,能够看做是方式二的简化形式。dom
方式四和方式五本质也是同样的,都是使用Java 8新增的 forEach
方法来遍历。方式五是方式四的一种简化形式,使用了Lambda表达式。ide
先用非线程安全的 ArrayList
作个试验,用一个线程经过加强的 for
循环遍历 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
。spa
当一个 List
正在经过迭代器遍历时,同时另一个线程对这个 List
进行修改,就会发生异常。线程
ArrayList
是非线程安全的,Vector
是线程安全的,那么把 ArrayList
换成 Vector
是否是就能够线程安全地遍历了?code
将程序中的:接口
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
是 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
方法是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)