在講如何線程安全地遍歷 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)