在多线程程序中更新共享资源
Updating a Shared Resource in Multithreaded Program
谁能解释一下下面程序的输出:
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, 2, 8]
- [2]
我无法理解输出中不同数量的值。我希望主线程要么等到所有线程都完成执行,因为我将它们加入 try-catch 块,然后输出四个值,每个线程一个,或者在中断的情况下打印到控制台。这两者都没有真正发生在这里。
如果这是由于多线程中的数据争用,它如何发挥作用?
主要问题是多个线程正在向同一个共享ArrayList
并发添加内容。 ArrayList is not thread-safe. From source 可以阅读:
Note that this implementation is not synchronized.
If multiple threads
access an ArrayList instance concurrently, and at least one of the
threads modifies the list structurally, it must be synchronized
externally. (A structural modification is any operation that adds or
deletes one or more elements, or explicitly resizes the backing array;
merely setting the value of an element is not a structural
modification.) This is typically accomplished by synchronizing on some
object that naturally encapsulates the list. If no such object exists,
the list should be "wrapped" using the Collections.synchronizedList
method. This is best done at creation time, to prevent accidental
unsynchronized access to the list:
每次调用时都在你的代码中
arr.add(local);
在 add
方法实现中,除其他外,跟踪数组 size
的变量将被更新。下面显示了 ArrayList
:
的 add
方法的相关部分
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
方法 同步化 和变量 size
都没有标记 volatile条款。因此,适合 race-conditions.
因此,因为您没有 ensure mutual exclusion 访问 ArrayList
(例如, 围绕 ArrayList
的调用synchronized 子句),并且因为 ArrayList
不能确保 size
变量被更新 atomically,每个线程可能会看到(或看不到)该变量的最后更新值。因此,线程可能会看到 size
变量的过时值,并将元素添加到其他线程之前已经添加的位置。在 extreme 中,所有线程最终可能会将一个元素添加到同一位置(例如, 作为您的输出之一 [2]
).
前面提到的 race-condition 导致 undefined behavior,因此原因:
System.out.println(DataRace.arr);
在不同的代码执行中输出不同数量的元素。
要使 ArrayList
线程安全或替代方案,请查看以下 SO 线程:How do I make my ArrayList Thread-Safe?, where it showcases the use of 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);
}
我认为有很多类似或密切相关的问题。例如参见 [=14=].
基本上这种“意外”行为的原因是因为 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.
TL;DR
ArrayList
不是 Thread-Safe。因此,它在竞争条件下的行为是未定义的。请改用 synchronized
或 CopyOnWriteArrayList
。
更长的答案
ArrayList.add
最终调用了这个私有方法:
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
当两个线程在“同一”时间到达同一点时,它们将具有相同的大小 (s
),并且都将尝试在同一位置添加一个元素并将大小更新为 s + 1
,因此很可能保留第二个的结果。
如果达到 ArrayList
的大小限制,并且必须达到 grow()
,则会创建一个更大的新数组并复制内容,这可能会导致 concurrently
所做的任何其他更改丢失(多个线程可能会尝试 grow
).
此处的备选方案是使用 monitors - a.k.a。 synchronized
,或使用线程安全替代方案,如 CopyOnWriteArrayList
。
谁能解释一下下面程序的输出:
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, 2, 8]
- [2]
我无法理解输出中不同数量的值。我希望主线程要么等到所有线程都完成执行,因为我将它们加入 try-catch 块,然后输出四个值,每个线程一个,或者在中断的情况下打印到控制台。这两者都没有真正发生在这里。
如果这是由于多线程中的数据争用,它如何发挥作用?
主要问题是多个线程正在向同一个共享ArrayList
并发添加内容。 ArrayList is not thread-safe. From source 可以阅读:
Note that this implementation is not synchronized.
If multiple threads access an ArrayList instance concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements, or explicitly resizes the backing array; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list. If no such object exists, the list should be "wrapped" using the Collections.synchronizedList method. This is best done at creation time, to prevent accidental unsynchronized access to the list:
每次调用时都在你的代码中
arr.add(local);
在 add
方法实现中,除其他外,跟踪数组 size
的变量将被更新。下面显示了 ArrayList
:
add
方法的相关部分
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
方法 同步化 和变量 size
都没有标记 volatile条款。因此,适合 race-conditions.
因此,因为您没有 ensure mutual exclusion 访问 ArrayList
(例如, 围绕 ArrayList
的调用synchronized 子句),并且因为 ArrayList
不能确保 size
变量被更新 atomically,每个线程可能会看到(或看不到)该变量的最后更新值。因此,线程可能会看到 size
变量的过时值,并将元素添加到其他线程之前已经添加的位置。在 extreme 中,所有线程最终可能会将一个元素添加到同一位置(例如, 作为您的输出之一 [2]
).
前面提到的 race-condition 导致 undefined behavior,因此原因:
System.out.println(DataRace.arr);
在不同的代码执行中输出不同数量的元素。
要使 ArrayList
线程安全或替代方案,请查看以下 SO 线程:How do I make my ArrayList Thread-Safe?, where it showcases the use of 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);
}
我认为有很多类似或密切相关的问题。例如参见 [=14=].
基本上这种“意外”行为的原因是因为 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.
TL;DR
ArrayList
不是 Thread-Safe。因此,它在竞争条件下的行为是未定义的。请改用 synchronized
或 CopyOnWriteArrayList
。
更长的答案
ArrayList.add
最终调用了这个私有方法:
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
当两个线程在“同一”时间到达同一点时,它们将具有相同的大小 (s
),并且都将尝试在同一位置添加一个元素并将大小更新为 s + 1
,因此很可能保留第二个的结果。
如果达到 ArrayList
的大小限制,并且必须达到 grow()
,则会创建一个更大的新数组并复制内容,这可能会导致 concurrently
所做的任何其他更改丢失(多个线程可能会尝试 grow
).
此处的备选方案是使用 monitors - a.k.a。 synchronized
,或使用线程安全替代方案,如 CopyOnWriteArrayList
。