保证Java执行框架中并发任务的副作用的可见性

时间:2015-02-10 18:01:35

标签: java concurrency synchronization locks

我正在尝试使用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;
    }
    ...
}

这种替代方案效率更高,但它和使用许多锁的先前实现对我来说都是正确的。我的观察都是正确的吗?在我的场景中是否有更有效/更优雅的方法来处理同步问题?

1 个答案:

答案 0 :(得分:0)

result声明为volatile只会确保每个人都可以看到更改result(即result = ...;操作)的引用。

解决此问题最明显的方法是消除副作用。在这种情况下,这很简单:只需使calculateCell()Callable调用它返回值,让主线程将值写入数组。

你当然可以进行显式锁定,就像你在第二个例子中所做的那样,但是当你只使用一个锁时,使用nxm锁似乎有点过分。当然,一个锁会破坏你的例子中的并行性,所以解决方法是再次使calculateCell()返回值,并且仅在result数组中写入结果的时间内锁定。

或者你确实可以使用ForkJoin并忘记整件事,因为它会为你做。