在讲如何线程安全地遍历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作个试验,用一个线程遍历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.ConcurrentModificationExceptionspa
线程一在遍历到第二个元素时,线程二删除了一个元素,此时程序出现异常:ConcurrentModificationException。线程
试想若是一个老师正在点整个班级全部学生的人数(线程一遍历List),而校长(线程二)同时叫走几个学生,那么老师也确定点不下去了。code
因此咱们会想到一个解决方案,那就是校长等待老师点完学生后,再叫走学生。即让线程二等待线程一的遍历完成后再进行remove元素。blog
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是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)
转载请注明原文地址:http://xxgblog.com/2016/04/02/traverse-list-thread-safe/