考虑以下代码。
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都完成了计算,然后检索结果并对其进行处理。
使用 get
和 set
方法 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。
那么,后面的代码是否会产生和上面一样的结果呢?
那么,后一种代码是否会产生与上述相同的结果呢?
会的。
你所读到的 "某处"。Thread.join
可能是 JLS 17.4.5 (Java内存模型的 "Happens-before order "位)。
线程中的所有操作都会在其他线程成功返回之前发生。
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 */;
}
}
这使得代码更简单,更明显正确,因为你不用担心内存的一致性:这是由框架处理的。这也给了你更多的灵活性,例如在比工作项更少的线程上运行,重复使用线程池等。
当多个线程访问同一个内存位置时,需要原子操作来确保内存壁垒的存在。如果没有内存壁垒,线程之间就没有发生过之前的关系,也不能保证主线程会看到其他线程所做的修改,因此会出现数据漫游。所以你真正需要的是写和读操作的内存壁垒。你可以使用AtomicReferenceArray或一个普通对象上的同步块来实现。
你有 Thread.join
在第二个程序中,在读取操作之前。这样应该可以消除数据竞赛。如果没有 join
,你需要明确的同步。