遍歷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<Object>() {
@Override
public void accept(Object item) {
System.out.println(item);
}
});
方式五(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<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
正在通過迭代器遍歷時,同時另外一個線程對這個 List
進行修改,就會發生異常。
使用線程安全的Vector
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
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
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)
關注我