我正在尝试使用Java Executor框架执行的并发任务来确保副作用可见性的技术。 作为一个简单的场景,考虑矩阵乘法的假设问题。
让我们说要乘法的矩阵可能相当大(例如,几千行和几列),并且为了加速这些矩阵的乘法,我实现了一个并发算法,其中计算每个单元格。结果矩阵被认为是一个独立的(即可并行化的)任务。 为了简化一点,让我们忽略对于小输入矩阵,这种并行化可能不是一个好主意。
在我的程序的第一个版本下面考虑一下:
public class MatrixMultiplier {
private final int[][] m;
private final int[][] n;
private volatile int[][] result; //the (lazily computed) result of the matrix multiplication
private final int numberOfMRows; //number of rows in M
private final int numberOfNColumns; //number of columns in N
private final int commonMatrixDimension; //number of columns in M and rows in N
public MatrixMultiplier(int[][] m, int[][] n) {
if(m[0].length != n.length)
throw new IllegalArgumentException("Uncompatible arguments: " + Arrays.toString(m) + " and " + Arrays.toString(n));
this.m = m;
this.n = n;
this.numberOfMRows = m.length;
this.numberOfNColumns = n[0].length;
this.commonMatrixDimension = n.length;
}
public synchronized int[][] multiply() {
if (result == null) {
result = new int[numberOfMRows][numberOfNColumns];
ExecutorService executor = createExecutor();
Collection<Callable<Void>> tasks = new ArrayList<>();
for (int i = 0; i < numberOfMRows; i++) {
final int finalI = i;
for (int j = 0; j < numberOfNColumns; j++) {
final int finalJ = j;
tasks.add(new Callable<Void>() {
@Override
public Void call() throws Exception {
calculateCell(finalI, finalJ);
return null;
}
});
}
}
try {
executor.invokeAll(tasks);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
executor.shutdownNow();
}
}
return result;
}
private ExecutorService createExecutor() {
final int availableProcessors = Runtime.getRuntime().availableProcessors();
final int processorsBound = availableProcessors + 1;
final int maxConcurrency = numberOfMRows * numberOfNColumns;
final int threadPoolSize = maxConcurrency < processorsBound ? maxConcurrency : processorsBound;
return Executors.newFixedThreadPool(threadPoolSize);
}
private void calculateCell(int mRow, int nColumn) {
int sum = 0;
for (int k = 0; k < commonMatrixDimension; k++) {
sum += m[mRow][k] * n[k][nColumn];
}
result[mRow][nColumn] = sum;
}
}
据我所知,此实现存在问题:执行任务对result
矩阵的某些修改可能不一定对调用multiply()
的线程可见。
假设前一个是正确的,请考虑依赖于显式锁的multiply()
的替代实现(新的锁相关代码用//<LRC>
注释):
public synchronized int[][] multiply() {
if (result == null) {
result = new int[numberOfMRows][numberOfNColumns];
final Lock[][] locks = new Lock[numberOfMRows][numberOfNColumns]; //<LRC>
for (int i = 0; i < numberOfMRows; i++) { //<LRC>
for (int j = 0; j < numberOfNColumns; j++) { //<LRC>
locks[i][j] = new ReentrantLock(); //<LRC>
} //<LRC>
} //<LRC>
ExecutorService executor = createExecutor();
Collection<Callable<Void>> tasks = new ArrayList<>();
for (int i = 0; i < numberOfMRows; i++) {
final int finalI = i;
for (int j = 0; j < numberOfNColumns; j++) {
final int finalJ = j;
tasks.add(new Callable<Void>() {
@Override
public Void call() throws Exception {
try { //<LRC>
locks[finalI][finalJ].lock(); //<LRC>
calculateCell(finalI, finalJ);
} finally { //<LRC>
locks[finalI][finalJ].unlock(); //<LRC>
} //<LRC>
return null;
}
});
}
}
try {
executor.invokeAll(tasks);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
executor.shutdownNow();
}
for (int i = 0; i < numberOfMRows; i++) { //<LRC>
for (int j = 0; j < numberOfNColumns; j++) { //<LRC>
locks[i][j].lock(); //<LRC>
locks[i][j].unlock(); //<LRC>
} //<LRC>
} //<LRC>
}
return result;
}
上面使用显式锁具有唯一的目标,以确保发布对调用线程的更改,因为没有任何争用的可能性。
我的主要问题是,这是否是我方案中发布副作用问题的有效解决方案。
作为第二个问题:是否有更有效/更优雅的方法来解决这个问题?请注意,我不是在寻找用于并行化矩阵乘法的替代算法实现(例如,Strassen算法),因为我的只是一个简单的案例研究。我对确保算法变化可见性的替代方案很感兴趣,如此处所示。
更新
我认为下面的替代实现改进了之前的实现。它使用一个单独的内部锁而不会影响并发性:
public class MatrixMultiplier {
...
private final Object internalLock = new Object();
public synchronized int[][] multiply() {
if (result == null) {
result = new int[numberOfMRows][numberOfNColumns];
ExecutorService executor = createExecutor();
Collection<Callable<Void>> tasks = new ArrayList<>();
for (int i = 0; i < numberOfMRows; i++) {
final int finalI = i;
for (int j = 0; j < numberOfNColumns; j++) {
final int finalJ = j;
tasks.add(new Callable<Void>() {
@Override
public Void call() throws Exception {
calculateCell(finalI, finalJ);
synchronized (internalLock){}
return null;
}
});
}
}
try {
executor.invokeAll(tasks);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
executor.shutdownNow();
}
}
synchronized (internalLock){}
return result;
}
...
}
这种替代方案效率更高,但它和使用许多锁的先前实现对我来说都是正确的。我的观察都是正确的吗?在我的场景中是否有更有效/更优雅的方法来处理同步问题?
答案 0 :(得分:0)
将result
声明为volatile
只会确保每个人都可以看到更改result
(即result = ...;
操作)的引用。
解决此问题最明显的方法是消除副作用。在这种情况下,这很简单:只需使calculateCell()
和Callable
调用它返回值,让主线程将值写入数组。
你当然可以进行显式锁定,就像你在第二个例子中所做的那样,但是当你只使用一个锁时,使用nxm
锁似乎有点过分。当然,一个锁会破坏你的例子中的并行性,所以解决方法是再次使calculateCell()
返回值,并且仅在result
数组中写入结果的时间内锁定。
或者你确实可以使用ForkJoin
并忘记整件事,因为它会为你做。