ЗавершенныеFuture, изменяемые объекты и видимость памяти

Я пытаюсь понять, как CompletableFuture в Java 8 взаимодействует с моделью памяти Java. Мне кажется, что для разумного программиста в идеале должно быть справедливо следующее:

  • Выполняются действия в потоке, завершающем CompletableFuture - перед выполнением любых завершений зависимых этапов
  • Действия в потоке, который регистрирует завершение, создает зависимый этап - до выполнения этапа <завершения > завершения

В документе java.util.concurrent есть примечание, в котором говорится:

Действия в потоке перед отправкой Runnable в Executor произойдет до того, как начнется его выполнение. Аналогично для Callable, представленного ExecutorService.

Что бы предположить, что первое свойство является истинным, если поток, завершающий будущее, выполняет этап completion или отправляет его в Executor. С другой стороны, после чтения CompletableFuture документации я не уверен в этом:

Действия, предоставляемые для зависимых завершений неасинхронных методов, могут выполняться потоком, который завершает текущий CompletableFuture, или любым другим вызывающим абонентом метода завершения.

Что подводит меня к моим вопросам:

  • Являются ли два гипотетических свойства выше true или нет?
  • Существует ли какая-либо конкретная документация в отношении наличия или отсутствия видимости памяти при работе с CompletableFuture?

Добавление:

В качестве конкретного примера рассмотрим этот код:

List<String> list1 = new ArrayList<>();
list1.add("foo");

CompletableFuture<List<String>> future =
        CompletableFuture.supplyAsync(() -> {
            List<String> list2 = new ArrayList<>();
            list2.addAll(list1);
            return list2;
        });

Гарантируется ли возможность добавления "foo" в list1 функции лямбда? Гарантируется ли возможность добавления list1 в list2 зависимым этапам future?

Ответы

Ответ 1

  • Да, обе ваши гипотезы верны. Причина в том, что все методы *Async() в CompletableFuture будут использовать java.util.concurrent.Executor для выполнения асинхронного вызова. Если вы его не предоставите, это будет либо общий пул, либо Исполнитель, который создает новый поток для каждой задачи (если вы ограничиваете размер общего пула 0 или 1) или предоставленным пользователем Исполнителем. Как вы уже узнали, документация Executor говорит:

    Действия в потоке перед отправкой объекта Runnable в Executor происходят до того, как его выполнение начинается, возможно, в другом потоке.

    Итак, в вашем примере гарантируется, что "foo" является частью list1 в вашей лямбда и что list2 отображается на последующих этапах.

  • В основном это связано с документацией Executor.