在多线程程序中更新共享资源

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

    }
}

输出:


我无法理解输出中不同数量的值。我希望主线程要么等到所有线程都完成执行,因为我将它们加入 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。因此,它在竞争条件下的行为是未定义的。请改用 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).

此处的备选方案是使用 monitors - a.k.a。 synchronized,或使用线程安全替代方案,如 CopyOnWriteArrayList