Нужно ли мне синхронизировать результат вызова invokeAll?

Я улучшаю существующий алгоритм, который состоит из нескольких независимых шагов для использования параллельных задач. Каждая из задач создаст несколько объектов для хранения своих результатов. В конце концов, я хотел бы иметь список всех результатов для возврата из метода управления. На данный момент мой код выглядит примерно так.

private final ExecutorService pool = ...;

// A single task to be performed concurrently with other tasks.
private class WorkHorse implements Callable<Void> {
    private final Collection<X> collect;

    public WorkHorse(Collection<X> collect, ...) {
        this.collect = collect;
    }

    public Void call() {
        for (...) {
            // do work

            synchronized (this.collect) {
                this.collect.add(result);
            }
        }
        return null;
    }
}

// Uses multiple concurrent tasks to compute its result list.
public Collection<X> getResults() {
    // this list is supposed to hold the results
    final Collection<X> collect = new LinkedList<X>();

    final List<WorkHorse> tasks = Arrays.asList(  
        new WorkHorse(collect, ...), new WorkHorse(collect, ...), ...);
    this.pool.invokeAll(tasks);

    // ## A ##
    synchronized (collect) {
        return collect;
    }
}

Я действительно нуждаюсь в synchronized в "## A ##", чтобы принудительно установить связь с изменениями в рабочих задачах? Или я могу полагаться на все операции записи, которые произошли после того, как invokeAll вернется и будет видимым для управляющего потока? И есть ли какая-то причина, почему я не должен возвращать коллекцию результатов из своего собственного блока synchronized?

Ответ 1

Нет, вам это не нужно. В документации invokeAll указано, что все задания должны выполняться при их возврате. Таким образом, не должно быть никакого дополнительного доступа для сбора, когда вы достигнете оператора return.

Ответ 2

Вам не нужен второй synchronized, если у вас есть первый. Как отмечает Зед, invokeAll() будет блокироваться, пока все задачи не будут завершены. Между тем, синхронизация вокруг add() гарантирует, что изменения в коллекции будут видны для всех потоков, включая исходный вызывающий поток.

Что касается того, нужен ли вам первый (который вы не спрашивали), я попытался удалить оба блока synchronized и не смог на самом деле заставить его потерпеть неудачу, но наличие его там, вероятно, является более безопасной ставкой. Согласно javadoc для LinkedList:

Если несколько потоков обращаются к LinkedList одновременно и, по крайней мере, один из потоков изменяет список структурно, это должно быть синхронизированный снаружи.

Другие реализации "2-го поколения" Collection имеют похожие предупреждения.

Заметим, кстати, что нет ничего удивительного в синхронизации самой коллекции. Вы можете объявить отдельный мьютекс (любой старый Object будет делать) во внешнем классе или синхронизировать с внешним экземпляром класса, и это будет работать так же хорошо, если все WorkHorse синхронизируются на одном и том же.