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

问题描述 投票:0回答:1

考虑以下代码。

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都完成了计算,然后检索结果并对其进行处理。

使用 getset 方法 AtomicReferenceArray 提供了一个内存排序,保证主线程能看到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]; 应该看到由 i-th MyThread。

那么,后面的代码是否会产生和上面一样的结果呢?

java arrays multithreading java-memory-model
1个回答
3
投票

那么,后一种代码是否会产生与上述相同的结果呢?

会的。

你所读到的 "某处"。Thread.join 可能是 JLS 17.4.5 (Java内存模型的 "Happens-before order "位)。

线程中的所有操作都会在其他线程成功返回之前发生。join() 的线程上。

所以,你对单个元素的所有写入都会在最终的 join().

说到这里,我强烈建议你寻找其他的方法来构建你的问题,而不需要你担心你的代码在这个细节水平上的正确性(见我的 其他答案).


2
投票

这里一个更简单的解决方案似乎是使用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 */;
    }

}

这使得代码更简单,更明显正确,因为你不用担心内存的一致性:这是由框架处理的。这也给了你更多的灵活性,例如在比工作项更少的线程上运行,重复使用线程池等。


0
投票

当多个线程访问同一个内存位置时,需要原子操作来确保内存壁垒的存在。如果没有内存壁垒,线程之间就没有发生过之前的关系,也不能保证主线程会看到其他线程所做的修改,因此会出现数据漫游。所以你真正需要的是写和读操作的内存壁垒。你可以使用AtomicReferenceArray或一个普通对象上的同步块来实现。

你有 Thread.join 在第二个程序中,在读取操作之前。这样应该可以消除数据竞赛。如果没有 join,你需要明确的同步。

© www.soinside.com 2019 - 2024. All rights reserved.