1. 概述

本文深入探讨 Java 中 Thread 类提供的 join() 方法。我们会分析它的行为机制、使用场景以及注意事项,并结合代码示例帮助理解。

wait()notify() 一样,join() 是实现线程间同步的重要手段之一。它能让一个线程等待另一个线程执行完毕后再继续,是并发编程中非常基础但关键的操作。

如果你对 wait/notify 机制还不熟悉,可以先参考 Java wait/notify 详解

2. Thread.join() 基本用法

join() 方法定义在 Thread 类中:

public final void join() throws InterruptedException
等待该线程死亡(终止)。

核心行为:
当你在一个线程对象上调用 join()调用方线程会进入阻塞状态,直到目标线程运行结束

来看一个直观的例子:

class SampleThread extends Thread {
    public int processingCount = 0;

    SampleThread(int processingCount) {
        this.processingCount = processingCount;
        LOGGER.info("Thread Created");
    }

    @Override
    public void run() {
        LOGGER.info("Thread " + this.getName() + " started");
        while (processingCount > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                LOGGER.info("Thread " + this.getName() + " interrupted");
            }
            processingCount--;
            LOGGER.info("Inside Thread " + this.getName() + ", processingCount = " + processingCount);
        }
        LOGGER.info("Thread " + this.getName() + " exiting");
    }
}

@Test
public void givenStartedThread_whenJoinCalled_waitsTillCompletion() 
  throws InterruptedException {
    Thread t2 = new SampleThread(1);
    t2.start();
    LOGGER.info("Invoking join");
    t2.join();
    LOGGER.info("Returned from join");
    assertFalse(t2.isAlive());
}

执行后输出大致如下:

[main] INFO: Thread Thread-1 Created
[main] INFO: Invoking join
[Thread-1] INFO: Thread Thread-1 started
[Thread-1] INFO: Inside Thread Thread-1, processingCount = 0
[Thread-1] INFO: Thread Thread-1 exiting
[main] INFO: Returned from join

可以看到,main 线程在 join() 处被阻塞,直到 Thread-1 执行完毕才继续。

⚠️ 注意几个关键点:

  • 如果目标线程被中断(interrupted),join() 会抛出 InterruptedException
  • 如果目标线程已经结束,或者压根没调用 start(),那么 join()立即返回,不会阻塞。
Thread t1 = new SampleThread(0);
t1.join();  // 立即返回,因为线程无需执行

这个特性在编写健壮的并发代码时很重要,避免不必要的等待。

3. 带超时的 join() 方法

有时候我们不能无限等待。如果目标线程卡住或执行时间过长,调用线程可能会一直阻塞,影响系统响应性。

为此,Thread 提供了两个带超时的 join() 重载方法:

public final void join(long millis) throws InterruptedException
最多等待 millis 毫秒。如果传 0,表示无限等待。

public final void join(long millis, int nanos) throws InterruptedException
最多等待 millis 毫秒 + nanos 纳秒。

实际开发中,join(long millis) 更常用,nanos 参数基本没人用,主要是历史遗留。

使用示例

@Test
public void givenStartedThread_whenTimedJoinCalled_waitsUntilTimedout()
  throws InterruptedException {
    Thread t3 = new SampleThread(10);
    t3.start();
    t3.join(1000);  // 最多等 1 秒
    assertTrue(t3.isAlive());  // 10 秒任务没完,线程仍存活
}

⚠️ 重要提醒:
超时时间依赖操作系统调度,不能保证精确。 也就是说,join(1000) 可能等 990ms,也可能等 1050ms,不要对精度有太高期待。

适用场景:

  • 防止线程无限挂起
  • 实现简单的超时控制
  • 避免死锁或资源耗尽

4. join() 与内存可见性(Happens-Before)

很多人只知道 join() 能“等线程结束”,但忽略了它更重要的作用:保证内存可见性

根据 JMM(Java 内存模型),join() 会建立一个 happens-before 关系:

“一个线程内的所有操作,happens-before 于其他任何线程成功从对该线程的 join() 调用中返回。”

通俗理解:
当线程 A 调用 t2.join() 并返回后,线程 t2 中修改的所有变量,对线程 A 都是可见的

踩坑示例:缺少同步的代码

SampleThread t4 = new SampleThread(10);
t4.start();
// ❌ 危险!无法保证可见性
do {
       
} while (t4.processingCount > 0);

问题出在哪?

  • 即使 t4 已经执行完毕,main 线程可能因为 CPU 缓存或编译器优化,读到的是 processingCount 的旧值
  • 这个循环可能永远无法退出,即使逻辑上任务已完成。

正确做法

✅ 方式一:使用 join()

t4.start();
t4.join();  // 既等待结束,又确保可见性
// 此时 processingCount 的值一定是最终值

✅ 方式二:使用 volatile + 显式同步

volatile int processingCount; // 保证可见性
// ... 配合 join 或其他机制

⚠️ 关键结论:
即使你知道线程已经结束,为了确保数据可见性,仍需调用 join() 或使用其他同步手段。不能依赖“线程结束了变量就自动可见”这种错误认知。

5. 总结

join() 不只是一个“等待线程结束”的简单工具,它在并发编程中有两个核心价值:

  • 线程协作:控制执行顺序,实现串行化等待。
  • 内存同步:建立 happens-before 关系,确保变量修改对调用线程可见。

使用建议

场景 推荐做法
必须等线程完成 join()
可能卡住的长任务 join(timeout)
需要获取线程结果 配合 FutureCallable
高频或复杂同步 考虑 CountDownLatchCyclicBarrier 等更高级工具

源码参考: 完整示例代码见 GitHub 仓库


原始标题:The Thread.join() Method in Java