问题描述
谁能解释一下下面程序的输出:
public class Datarace extends Thread {
static ArrayList<Integer> arr = new ArrayList<>();
public void run() {
Random random = new Random();
int local = random.nextInt(10) + 1;
arr.add(local);
}
public static void main(String[] args) {
Datarace t1 = new Datarace();
Datarace t2 = new Datarace();
Datarace t3 = new Datarace();
Datarace t4 = new Datarace();
t1.start();
t2.start();
t3.start();
t4.start();
try {
t1.join();
t2.join();
t3.join();
t4.join();
} catch (InterruptedException e) {
System.out.println("interrupted");
}
System.out.println(Datarace.arr);
}
}
输出:
- [8,5]
- [9,2,8]
- [2]
我无法理解输出中不同数量的值。我希望主线程要么等到所有线程都完成执行,因为我在 try-catch 块中加入它们,然后输出四个值,每个线程一个,或者在中断的情况下打印到控制台。这两种情况都没有在这里真正发生。
如果这是由于多线程中的数据竞争导致的,它在这里如何发挥作用?
解决方法
主要问题是多个线程同时添加到同一个共享 ArrayList
。 ArrayList 不是线程安全的。从 source 可以阅读:
请注意,此实现不是同步的。
如果多个线程
同时访问一个 ArrayList 实例,并且至少有一个
线程在结构上修改列表,它必须是同步的
外部。 (结构修改是任何添加或
删除一个或多个元素,或显式调整后备数组的大小;
仅仅设置元素的值不是结构性的
修改。)这通常是通过同步一些
自然封装列表的对象。如果不存在这样的对象,
该列表应该使用 Collections.synchronizedList 进行“包装”
方法。这最好在创建时完成,以防止意外
对列表的非同步访问:
每次调用时都在代码中
arr.add(local);
在 add
方法实现中,将更新跟踪数组 size
的变量。下面显示了 add
的 ArrayList
方法的相关部分:
private void add(E e,Object[] elementData,int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1; // <--
}
其中变量字段 size
是:
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
请注意,add
方法 synchronized 和变量 size
都没有用 volatile 子句标记。因此,适用于竞争条件。
因此,因为您没有 ensure mutual exclusion 对该 ArrayList
的访问(例如, 将 ArrayList
的调用与 同步 子句),并且因为 ArrayList
不能确保 size
变量原子地更新,每个线程可能会看到(或不)最后更新的值那个变量。因此,线程可能会看到 size
变量的过时值,并将元素添加到其他线程之前已添加的位置。在极端中,所有线程可能最终都会将一个元素添加到同一位置(例如,作为您的输出之一[2]
)。
上述竞争条件导致undefined behavior,因此原因:
System.out.println(DataRace.arr);
在代码的不同执行中输出不同数量的元素。
要使 ArrayList
线程安全或替代,请查看以下 SO 线程:How do I make my ArrayList Thread-Safe?,其中展示了 Collections.synchronizedList().、CopyOnWriteArrayList 的使用其他。
确保对 arr
结构的访问互斥的示例:
public void run() {
Random random = new Random();
int local = random.nextInt(10) + 1;
synchronized (arr) {
arr.add(local);
}
}
或:
static final List<Integer> arr = Collections.synchronizedList(new ArrayList<Integer>());
public void run() {
Random random = new Random();
int local = random.nextInt(10) + 1;
arr.add(local);
}
,
TL;DR
ArrayList
不是 Thread-Safe。因此,它在竞争条件下的行为是未定义的。改用 synchronized
或 CopyOnWriteArrayList
。
更长的答案
ArrayList.add
最终调用这个私有方法:
private void add(E e,int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
当两个线程在“同一”时间到达同一点时,它们将具有相同的大小(s
),并且都将尝试在同一位置添加一个元素并将大小更新为{{1 }},因此可能会保留第二个的结果。
如果达到 s + 1
的大小限制,并且必须达到 ArrayList
,则会创建一个新的更大的数组并复制内容,可能会导致 grow()
所做的任何其他更改丢失(多个线程可能会尝试concurrently
)。
此处的替代方案是使用 monitors - 又名 grow
,或使用线程安全的替代方案,例如 synchronized
。
我认为有很多类似或密切相关的问题。例如见this。
基本上这种“意外”行为的原因是因为 ArrayList
不是线程安全的。您可以尝试 List<Integer> arr = new CopyOnWriteArrayList<>()
,它会按预期工作。当我们想要频繁地执行读操作并且写操作的次数相对较少时,推荐使用这种数据结构。有关详细说明,请参阅 What is CopyOnWriteArrayList in Java - Example Tutorial。
另一种选择是使用 List<Integer> arr = Collections.synchronizedList(new ArrayList<>())
。
您也可以使用 Vector
,但不推荐使用(请参阅 here)。
这篇文章也很有用 - Vector vs ArrayList in Java。