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

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

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


以上是在多线程程序中更新共享资源的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>