Thread.join

worker.join() заставляет текущий поток ждать завершения worker.

Thread loader = new Thread(() -> loadCache(), "cache-loader");
loader.start();
loader.join();
useCache();

У join() есть не только координация, но и гарантия памяти: все действия завершившегося потока happens-before успешного возврата из join() в ожидающем потоке. Поэтому useCache() увидит записанные loader данные, если объект опубликован корректно.

Тайм-аут

loader.join(500);
if (loader.isAlive()) {
    loader.interrupt();
}

Возврат из timed join() не означает завершение: тайм-аут нужно отличать через isAlive() или более подходящую абстракцию результата.

join() бросает InterruptedException, если прервали ожидающий поток. Это сигнал отмены его ожидания, а не автоматическая остановка loader.

try {
    loader.join();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}

В прикладном коде чаще подходят Future.get(), CompletableFuture или структурированный жизненный цикл executor: они связывают ожидание с результатом и ошибкой.

На собеседовании

  • Какой поток блокируется при a.join()? Тот, который вызвал метод, а не a.
  • Что гарантирует обычный возврат из join()? Целевой поток завершен, а его действия видимы ожидающему.
  • Что гарантирует timed join()? Только то, что ожидание закончилось; поток мог остаться жив.
  • Освобождает ли join() мониторы, ранее захваченные вызывающим кодом? Нет; ждать под своей блокировкой опасно.