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) |
需要获取线程结果 | 配合 Future 或 Callable |
高频或复杂同步 | 考虑 CountDownLatch 、CyclicBarrier 等更高级工具 |
源码参考: 完整示例代码见 GitHub 仓库。