在多线程程序中更新共享资源
有人可以解释以下程序的输出:
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不是线程安全的。从源代码可以阅读:
请注意,此实现不是同步的。
如果多个线程同时访问一个 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方法既不是同步的,也不size是用volatile子句标记的变量。因此,适用于竞争条件。
因此,因为你没有 确保对访问的互斥ArrayList(例如,ArrayList使用synchronized子句包围对 the 的调用),并且因为ArrayList不能确保该size变量以原子方式更新,所以每个线程可能会看到(或不会)该变量的最后更新值。因此,线程可能会看到过时的size变量值,并将元素添加到其他线程之前已经添加的位置。在极端情况下,所有线程可能最终都会将一个元素添加到同一位置(例如,作为您的输出之一[2])。
上述竞争条件会导致未定义的行为,因此原因如下:
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);
}