Java: 当线程设置数组的不同单元格时是否需要易失性访问?

Java: Is volatile access necessary when threads set different cells of an array?

考虑以下代码:

public static void main(String[] args) throws InterruptedException {
    int nThreads = 10;
    MyThread[] threads = new MyThread[nThreads];

    AtomicReferenceArray<Object> array = new AtomicReferenceArray<>(nThreads);

    for (int i = 0; i < nThreads; i++) {
        MyThread thread = new MyThread(array, i);
        threads[i] = thread;
        thread.start();
    }

    for (MyThread thread : threads)
        thread.join();

    for (int i = 0; i < nThreads; i++) {
        Object obj_i = array.get(i);
        // do something with obj_i...
    }
}

private static class MyThread extends Thread {

    private final AtomicReferenceArray<Object> pArray;
    private final int pIndex;

    public MyThread(final AtomicReferenceArray<Object> array, final int index) {
        pArray = array;
        pIndex = index;
    }

    @Override
    public void run() {
        // some entirely local time-consuming computation...
        pArray.set(pIndex, /* result of the computation */);
    }

}

每个 MyThread 完全在本地计算某些内容(无需与其他线程同步)并将结果写入其特定的数组单元格。主线程等待所有 MyThreads 完成,然后检索结果并对其进行处理。

使用 AtomicReferenceArraygetset 方法提供内存排序,保证主线程将看到 MyThreads 写入的结果。

然而,由于每个数组单元只被写入一次,并且没有 MyThread 必须看到任何其他 MyThread 写入的结果,我想知道这些强顺序保证是否真的有必要,或者下面的代码是否具有普通数组单元访问,将保证始终产生与上述代码相同的结果:

public static void main(String[] args) throws InterruptedException {
    int nThreads = 10;
    MyThread[] threads = new MyThread[nThreads];

    Object[] array = new Object[nThreads];

    for (int i = 0; i < nThreads; i++) {
        MyThread thread = new MyThread(array, i);
        threads[i] = thread;
        thread.start();
    }

    for (MyThread thread : threads)
        thread.join();

    for (int i = 0; i < nThreads; i++) {
        Object obj_i = array[i];
        // do something with obj_i...
    }
}

private static class MyThread extends Thread {

    private final Object[] pArray;
    private final int pIndex;

    public MyThread(final Object[] array, final int index) {
        pArray = array;
        pIndex = index;
    }

    @Override
    public void run() {
        // some entirely local time-consuming computation...
        pArray[pIndex] = /* result of the computation */;
    }

}

一方面,在普通模式访问下,编译器或运行时可能会在主线程的最终循环中优化掉对 array 的读取访问,并将 Object obj_i = array[i]; 替换为 Object obj_i = null;(数组的隐式初始化),因为数组未从该线程内修改。另一方面,我在某处读到 Thread.join 使已加入线程的所有更改对调用线程可见(这是明智的),因此 Object obj_i = array[i]; 应该看到 [= 分配的对象引用21=]-th MyThread.

那么,后面的代码会产生与上面相同的结果吗?

当多个线程访问同一内存位置时,需要原子操作来确保存在内存屏障。没有内存屏障,线程之间就没有 happened-before 关系,也无法保证主线程会看到其他线程所做的修改,因此会发生数据竞争。所以你真正需要的是用于写入和读取操作的内存屏障。您可以使用 AtomicReferenceArray 或普通对象上的同步块来实现。

在读取操作之前,您在第二个程序中有 Thread.join。那应该消除数据竞争。没有 join,您需要显式同步。

此处似乎更简单的解决方案是使用 Executor 框架,它隐藏了通常不必要的有关线程和结果存储方式的详细信息。

例如:

ExecutorService executor = ...

List<Future<Object>> futures = new ArrayList<>();
for (int i = 0; i < nThreads; i++) {
  futures.add(executor.submit(new MyCallable<>(i)));
}
executor.shutdown();

for (int i = 0; i < nThreads; ++i) {
  array[i] = futures.get(i).get();
}

for (int i = 0; i < nThreads; i++) {
    Object obj_i = array[i];
    // do something with obj_i...
}

其中 MyCallable 类似于您的 MyThread:

private static class MyCallable implements Callable<Object> {

    private final int pIndex;

    public MyCallable(final int index) {
        pIndex = index;
    }

    @Override
    public Object call() {
        // some entirely local time-consuming computation...
        return /* result of the computation */;
    }

}

这会产生更简单且更明显正确的代码,因为您不必担心内存一致性:这是由框架处理的。它还为您提供了更大的灵活性,例如运行 它在比工作项更少的线程上,重用线程池等。

So, would the latter code produce the same results as the above?

是的。

您读到的关于 Thread.join 的 "somewhere" 可能是 JLS 17.4.5(Java 内存模型的 "Happens-before order" 位):

All actions in a thread happen-before any other thread successfully returns from a join() on that thread.

因此,您对单个元素的所有写入都将在最终 join() 之前发生。

话虽如此,我强烈建议您寻找其他方法来构建您的问题,这些方法不需要您担心代码在这个详细级别上的正确性(请参阅我的 ).