在講如何線程安全地遍歷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); }
list.forEach(new Consumer<Object>() { @Override public void accept(Object item) { System.out.println(item); } });
list.forEach(item -> { System.out.println(item); });
首先,方式一的遍歷方法是一種非常不建議使用的方式,特別是對於LinkedList。LinkedList是基於鏈表實現的,通過list.get(i)
獲取元素的性能差,所以方式一是性能很差的一種遍歷方式。本文接下來的內容不再討論這種方式的遍歷。
方式二和方式三兩種方式的本質是一樣的,都是通過Iterator迭代器來實現的遍歷,方式三可以看作是方式二的簡化形式。
方式四和方式五本質也是一樣的,都是使用Java 8新增的forEach方法來遍歷。方式五是方式四的一種簡化形式,使用了Lambda表達式。
先用非線程安全的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元素。
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/