Что гарантирует Java Memory Model потоку, который успешно дождался завершения другого потока через Thread.join()?
Все действия завершившегося потока, произошедшие до его завершения, устанавливают отношение happens-before к действиям потока после успешного возврата из Thread.join(). Поэтому поток, вызвавший join(), получает гарантии видимости результатов работы завершённого потока без дополнительной синхронизации.
Гарантия относится именно к успешному завершению ожидания. Сам факт вызова join() без возврата из него, например из-за прерывания, не даёт такого результата.
Многопоточные программы выполняются с участием оптимизаций компилятора, кэшей процессора и переупорядочивания операций. Поэтому завершение одного потока само по себе не должно рассматриваться как очевидный механизм публикации его результатов другому потоку.
Java Memory Model формализует такие гарантии через отношения порядка, включая happens-before. Для жизненного цикла потоков это позволяет безопасно передать результаты работы после завершения потока, не заставляя разработчика вручную защищать каждое поле отдельным механизмом.
Пусть рабочий поток вычисляет результат и записывает его в обычные, не volatile поля объекта. Затем управляющий поток должен прочитать этот результат после окончания работы.
Если управляющий поток просто проверяет косвенный признак или читает данные без установленного отношения happens-before, возникает риск увидеть устаревшие значения либо наблюдать состояние без требуемой публикации. Ошибка особенно опасна тем, что на одной машине программа может стабильно работать, а при другой нагрузке или архитектуре проявить некорректное поведение.
Нормальное возвращение из Thread.join() означает, что целевой поток уже завершился. Все действия целевого потока до завершения происходят happens-before действий вызывающего потока после возврата из join().
Следовательно, обычные записи, выполненные рабочим потоком до завершения, становятся видимыми управляющему потоку. Это не делает отдельные операции атомарными и не защищает данные, если после завершения рабочего потока их одновременно изменяют другие потоки.
Минимальный пример:
В примере запись result.value = 42 выполнена до завершения worker, а чтение происходит после успешного join(). Поэтому значение 42 гарантированно опубликовано вызывающему потоку; volatile для value в этом сценарии не требуется.
Гарантия не распространяется на записи, выполненные после чтения управляющим потоком, и не превращает составную операцию в транзакцию. Если join() прерван и метод не дождался завершения потока, нужно отдельно обработать прерывание и не считать результат опубликованным только на основании начатого ожидания.
Сервис запускает поток, который строит неизменяемый снимок конфигурации, после чего основной поток должен передать этот снимок дальнейшему коду. Рассматривались три варианта: сделать все поля снимка volatile, защищать каждое чтение общим блокировщиком или дождаться завершения построителя через join().
volatile для отдельных полей не решает задачу публикации составного объекта целиком и может усложнить модель данных. Общий lock над всеми операциями надёжен, но избыточен, если после построения снимок больше не изменяется.
Выбрано ожидание через join(): рабочий поток полностью формирует снимок, основной поток успешно дожидается его завершения и только затем использует объект. Это минимальное решение с ясной гарантией публикации; результатом стало отсутствие лишних блокировок и предсказуемая видимость всех записей, выполненных до завершения построителя.
join(), если он завершился исключением из-за прерывания?Нет. Гарантия относится к успешному возврату из join(), то есть к моменту, когда вызывающий поток действительно дождался завершения целевого потока. При InterruptedException ожидание прекращено досрочно; приложение должно выбрать политику обработки прерывания, например восстановить флаг прерывания и не использовать результат как опубликованный.
join() чтение результата атомарным?Нет. join() устанавливает порядок и видимость действий, но не превращает последующие операции в неделимые. Например, чтение нескольких изменяемых полей может быть логически несогласованным, если после завершения целевого потока другой поток продолжает менять эти поля. Для атомарности и согласованности нужны отдельные средства: блокировки, атомарные классы, неизменяемость или иной протокол доступа.
join() ожиданием через isAlive()?Проверка состояния потока сама по себе не должна использоваться как эквивалент установленного протокола публикации. Надёжная гарантия в рассматриваемом сценарии формулируется для успешного join(). Если нужен другой способ ожидания, следует применять механизм с документированными гарантиями happens-before, например Future.get() или корректно используемый CountDownLatch.