文章目录
  1. 1. 遍历List的多种方式
    1. 1.1. 方式一:
    2. 1.2. 方式二:
    3. 1.3. 方式三:
    4. 1.4. 方式四(Java 8):
    5. 1.5. 方式五(Java 8 Lambda):
  2. 2. 遍历List的同时操作List会发生什么?
  3. 3. 使用线程安全的Vector
  4. 4. CopyOnWriteArrayList
  5. 5. 线程安全的List.forEach

遍历List的多种方式

在讲如何线程安全地遍历 List 之前,先看看遍历一个 List 通常会采用哪些方式。

方式一:

1
2
3
for(int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}

方式二:

1
2
3
4
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}

方式三:

1
2
3
for(Object item : list) {
System.out.println(item);
}

方式四(Java 8):

1
2
3
4
5
6
list.forEach(new Consumer<Object>() {
@Override
public void accept(Object item) {
System.out.println(item);
}
});

方式五(Java 8 Lambda):

1
2
3
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 中的一个元素,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 是不是就可以线程安全地遍历了?

将程序中的:

1
final List<Integer> list = new ArrayList<>();

改成:

1
final List<Integer> list = new Vector<>();

再运行一次试试,会发现结果和 ArrayList 一样会抛出 ConcurrentModificationException 异常。

为什么线程安全的 Vector 也不能线程安全地遍历呢?其实道理也很简单,看 Vector 源码可以发现它的很多方法都加上了 synchronized 来进行线程同步,例如 add()remove()set()get(),但是 Vector 内部的 synchronized 方法无法控制到外部遍历操作,所以即使是线程安全的 Vector 也无法做到线程安全地遍历。

如果想要线程安全地遍历 Vector,需要我们去手动在遍历时给 Vector 加上 synchronized 锁,防止遍历的同时进行 remove 操作。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

CopyOnWriteArrayListjava.util.concurrent 包中的一个 List 的实现类。CopyOnWrite 的意思是在写时拷贝,也就是如果需要对CopyOnWriteArrayList 的内容进行改变,首先会拷贝一份新的 List 并且在新的 List 上进行修改,最后将原 List 的引用指向新的 List

使用 CopyOnWriteArrayList 可以线程安全地遍历,因为如果另外一个线程在遍历的时候修改 List 的话,实际上会拷贝出一个新的 List 上修改,而不影响当前正在被遍历的 List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 外遍历 ListforEach 方法相当于 List 自身遍历的方法,所以它可以自由控制是否线程安全。

我们看线程安全的 VectorforEach 方法源码:

1
2
3
public synchronized void forEach(Consumer<? super E> action) {
...
}

可以看到 VectorforEach 方法上加了 synchronized 来控制线程安全的遍历,也就是Vector的forEach方法可以线程安全地遍历

下面可以测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)

文章目录
  1. 1. 遍历List的多种方式
    1. 1.1. 方式一:
    2. 1.2. 方式二:
    3. 1.3. 方式三:
    4. 1.4. 方式四(Java 8):
    5. 1.5. 方式五(Java 8 Lambda):
  2. 2. 遍历List的同时操作List会发生什么?
  3. 3. 使用线程安全的Vector
  4. 4. CopyOnWriteArrayList
  5. 5. 线程安全的List.forEach