После успешного Future.get() какие изменения, выполненные внутри задачи, обязан увидеть вызывающий поток?
После успешного Future.get() вызывающий поток обязан видеть действия, выполненные асинхронной задачей до её завершения, включая записи в обычные поля. Это обеспечивается отношением happens-before между завершением задачи и возвратом соответствующего get().
Гарантия относится к успешному завершению именно той задачи, чьё состояние ожидается через данный Future. Она не превращает все последующие операции с общими данными в потокобезопасные.
До появления высокоуровневых средств многопоточности результат фоновой работы часто передавали через общие поля, флаги и ручные wait/notify. Такой подход требовал самостоятельно обеспечивать ожидание завершения, публикацию результата и обработку исключений.
Future в составе java.util.concurrent отделяет запуск задачи от получения её результата. Поток может передать работу исполнителю, а затем синхронно дождаться завершения через единый контракт, включающий ожидание, получение результата и уведомление об ошибке.
Окончание вычислений само по себе не означает, что другой поток автоматически увидит записи в обычные поля. Если поток просто проверяет несинхронизированный флаг завершения или читает общий объект без установленной связи happens-before, возможны устаревшие значения и наблюдение некорректного состояния.
Неверно также считать, что вызов get() делает саму задачу или используемые ею структуры данных потокобезопасными. Он устанавливает границу видимости между завершившейся задачей и ожидающим потоком, но не защищает от одновременных изменений, происходящих до завершения или после него.
Для задачи, отправленной через ExecutorService, действует следующая цепочка: действия вызывающего потока до submit() видимы задаче, а действия задачи до её завершения видимы потоку после успешного возврата из соответствующего Future.get().
Внутренне реализация future фиксирует завершённое состояние задачи с необходимой синхронизацией. Поток, выполняющий get(), либо ждёт завершения, либо сразу наблюдает уже установленное состояние; после успешного возврата он получает результат и гарантии видимости предшествующих действий задачи.
Здесь запись value = 42 выполнена задачей до её успешного завершения, а чтение происходит после get(). Поэтому вызывающий поток обязан увидеть 42, несмотря на то, что поле не объявлено volatile.
get() может завершиться неуспешно: при исключении задачи обычно выбрасывается ExecutionException, при прерывании ожидания — InterruptedException, а при отмене — CancellationException. Гарантию обычного результата нельзя приписывать коду после исключения из get() как к успешному завершению.
Если задача после публикации результата продолжает изменять общий объект в другом потоке, эти более поздние изменения данной гарантией не покрываются. Для повторяющегося совместного доступа всё равно нужны подходящие средства: volatile, блокировки, атомарные типы или потокобезопасные коллекции.
Сервис запускает расчёт конфигурации в пуле потоков и затем передаёт полученный объект обработчикам. В первом варианте поток запускал задачу, ждал несколько миллисекунд и читал общий обычный reference, надеясь, что вычисление уже закончилось. Такой код имел гонку: ожидание по времени не создавало отношения happens-before и могло вернуть старое или частично опубликованное состояние.
Второй вариант использовал volatile-ссылку и отдельный флаг. Это улучшало публикацию, но усложняло протокол: нужно было правильно устанавливать порядок записей и обрабатывать ошибки, отмену и повторный запуск.
Выбранным решением стал Future.get() перед использованием результата. Оно явно связывает чтение с завершением конкретной задачи, предоставляет стандартную обработку исключений и не требует отдельного флага. Если ожидание должно быть ограничено по времени, применяется get с тайм-аутом, после чего отдельно проектируется политика отмены или использования запасного результата.
Достаточно ли вызвать isDone(), чтобы получить такую же гарантию видимости?
Нет, полагаться на isDone() как на замену get() не следует. isDone() сообщает состояние future, но основной контракт гарантии публикации результата сформулирован для действий после успешного получения через Future.get().
Если необходимо использовать результат и получить связанные с этим гарантии, следует вызвать get(). Проверка isDone() может быть полезна для неблокирующей логики, но сама по себе не должна подменять корректный протокол получения результата.
Что увидит поток после get() с тайм-аутом, если время истекло?
При истечении тайм-аута get выбрасывает TimeoutException и не сообщает об успешном завершении задачи. Нельзя считать, что после такого вызова действия задачи уже опубликованы для продолжения текущего потока.
Задача при этом обычно продолжает выполняться: тайм-аут прекращает ожидание, но не отменяет работу. Если требуется остановить её, нужно отдельно вызвать cancel, понимая, что отмена через interrupt является запросом, а не гарантией немедленного завершения.
Гарантирует ли get() безопасную работу с результатом после его возврата?
Он гарантирует видимость состояния, сформированного задачей до завершения, но не делает сам результат неизменяемым или потокобезопасным. Если полученный объект после возврата используется несколькими потоками и кто-то его изменяет, необходима отдельная синхронизация.
Поэтому для результатов предпочтительны неизменяемые объекты или безопасная публикация снимка состояния. Если объект намеренно разделяется для дальнейших изменений, нужно определить протокол доступа с блокировками, volatile-ссылками, атомарными операциями или специализированными concurrent-структурами.