2

有人可以解释以下程序的输出:

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 块中,然后输出四个值,每个线程一个,或者在中断的情况下打印到控制台。这两者都没有真正发生在这里。

如果这是由于多线程中的数据竞争,它如何在这里发挥作用?

4

3 回答 3

5

主要问题是多个线程同时添加到同一个共享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使用同步子句围绕对该变量的最后更新值。因此,线程可能会看到变量的过时值,并将元素添加到其他线程之前已经添加的位置。在极端情况下,所有线程最终可能会将一个元素添加到同一位置(例如,作为您的输出之一)。ArrayListsizesize[2]

上述竞争条件导致未定义的行为,因此原因:

System.out.println(DataRace.arr);

在代码的不同执行中输出不同数量的元素。

要使ArrayList线程安全或替代方案,请查看以下 SO 线程:如何使我的 ArrayList 线程安全?,其中展示了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);
  }
于 2021-01-15T20:10:04.160 回答
1

TL;博士

ArrayList不是线程安全的。因此,它在竞态条件下的行为是未定义的。使用synchronizedCopyOnWriteArrayList代替。

更长的答案

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)。

这里的替代方案是使用监视器- 也就是synchronized使用 Thread-Safe 替代方案,如CopyOnWriteArrayList.

于 2021-01-15T20:19:01.670 回答
0

我认为有很多类似或密切相关的问题。例如看到这个

基本上,这种“意外”行为的原因是因为ArrayList它不是线程安全的。您可以尝试List<Integer> arr = new CopyOnWriteArrayList<>(),它会按预期工作。当我们要频繁执行读操作,而写操作的次数比较少时,推荐使用这种数据结构。有关详细说明,请参阅Java 中的 CopyOnWriteArrayList 是什么 - 示例教程

另一种选择是使用List<Integer> arr = Collections.synchronizedList(new ArrayList<>()).

您也可以使用Vector,但不建议使用(请参阅此处)。这篇文章也很有用——Java 中的 Vector vs ArrayList

于 2021-01-15T20:11:25.563 回答