跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java Thread.join() 方法详细解析

Java Thread.join() 方法,涵盖核心作用、三种重载形式及工作原理。通过对比 sleep() 说明锁释放差异,提供无参等待、超时等待及多线程串行/并行等待的实战代码示例。同时梳理异常处理、启动状态注意事项及底层源码逻辑,帮助开发者掌握线程同步机制。

灭霸发布于 2026/3/30更新于 2026/10/25103 浏览

一、核心定位

Thread.join() 是 Thread 类的实例方法,核心作用是实现线程间的同步(等待机制):当一个线程(如主线程)调用另一个目标线程(如子线程)的 join() 方法时,调用线程会进入阻塞状态,直到目标线程执行完毕(进入 TERMINATED 状态)或等待超时,才会继续执行自身后续代码。

简单说:A.join() 让「当前线程」等「线程 A」执行完,再继续自己的工作。

二、重载方法

Java 提供了 3 个重载的 join() 方法,满足不同等待需求:

方法签名功能说明
void join() throws InterruptedException无参重载:无限等待,调用线程会一直阻塞,直到目标线程完全执行终止(不会主动超时)
void join(long millis) throws InterruptedException带毫秒参数:超时等待,调用线程最多阻塞 millis 毫秒(千分之一秒),超时后自动唤醒继续执行
void join(long millis, int nanos) throws InterruptedException带毫秒 + 纳秒参数:高精度超时等待,理论上支持纳秒级精度,但实际受操作系统时钟精度限制(一般无需使用,优先用毫秒重载)
关键说明
  1. join(0) 等价于 join():表示「无限等待目标线程终止」,并非等待 0 毫秒。
  2. 纳秒参数(nanos)取值范围:0-999999,超出该范围会抛出 IllegalArgumentException。
  3. 所有重载方法均会抛出 InterruptedException(受检异常),必须捕获或声明抛出(因为等待中的线程可能被其他线程中断)。

三、工作原理

1. 核心逻辑

当线程 T1 调用线程 T2.join() 时,底层执行流程如下:

  1. T1(调用线程)会先判断 T2(目标线程)是否还处于存活状态(isAlive());
  2. 若 T2 仍存活,T1 会调用 Object.wait() 方法(底层依赖 Object 的等待机制),进入阻塞状态;
  3. 当 T2 执行完毕终止时,JVM 会自动调用 T2 的 notifyAll() 方法,唤醒所有等待在 T2 对象上的线程(即 T1);
  4. 若设置了超时时间,T1 在等待超时后会自动唤醒,无需 T2 终止。
2. 与 Thread.sleep() 的核心区别

很多人会混淆 join() 和 sleep(),二者的核心差异在于锁释放和使用场景:

特性
Thread.join()
Thread.sleep(long millis)
方法类型实例方法(针对目标线程)静态方法(针对当前线程)
锁处理阻塞时会释放当前线程持有的对象锁阻塞时不释放任何锁资源
等待状态无参:WAITING;带参:TIMED_WAITINGTIMED_WAITING
唤醒条件目标线程终止 / 等待超时 / 被中断时间到达 / 被中断
核心场景线程间同步(等待其他线程完成)当前线程暂停指定时间

四、实战示例

示例 1:无参 join() - 主线程等待子线程完全执行
public class JoinBasicDemo {
    public static void main(String[] args) {
        // 定义子线程:模拟耗时任务(打印 1-5,每次间隔 500ms)
        Thread subThread = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    Thread.sleep(500); // 模拟任务耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程执行中:i = " + i);
            }
            System.out.println("子线程执行完毕!");
        }, "子线程-Test");

        System.out.println("主线程启动子线程...");
        subThread.start(); // 启动子线程

        try {
            System.out.println("主线程开始等待子线程执行完毕...");
            subThread.join(); // 主线程阻塞,等待 subThread 执行完
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 子线程执行完后,主线程才会执行这里
        System.out.println("主线程继续执行,程序结束!");
    }
}
运行结果(顺序固定)
主线程启动子线程...
主线程开始等待子线程执行完毕...
子线程执行中:i = 1
子线程执行中:i = 2
子线程执行中:i = 3
子线程执行中:i = 4
子线程执行中:i = 5
子线程执行完毕!
主线程继续执行,程序结束!
示例 2:带超时 join(long millis) - 主线程等待超时后继续执行

修改示例 1 中的 join() 调用,设置超时时间 1500ms(子线程总耗时 2500ms):

public class JoinTimeoutDemo {
    public static void main(String[] args) {
        Thread subThread = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程执行中:i = " + i);
            }
            System.out.println("子线程执行完毕!");
        }, "子线程-Timeout");

        System.out.println("主线程启动子线程...");
        subThread.start();

        try {
            System.out.println("主线程开始等待子线程(超时 1500ms)...");
            subThread.join(1500); // 最多等待 1500ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 1500ms 后,主线程不再等待,直接执行后续代码
        System.out.println("主线程等待超时,继续执行自身逻辑!");
    }
}
运行结果(主线程超时后提前执行)
主线程启动子线程...
主线程开始等待子线程(超时 1500ms)...
子线程执行中:i = 1
子线程执行中:i = 2
子线程执行中:i = 3
主线程等待超时,继续执行自身逻辑!
子线程执行中:i = 4
子线程执行中:i = 5
子线程执行完毕!
示例 3:多个线程 join() - 串行等待 vs 并行等待
场景 1:串行等待(主线程依次等待多个子线程,子线程串行执行)
public class JoinSerialDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程 t1 执行中:i = " + i);
            }
        }, "线程 t1");

        Thread t2 = new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程 t2 执行中:i = " + i);
            }
        }, "线程 t2");

        t1.start();
        t1.join(); // 主线程先等 t1 执行完
        t2.start();
        t2.join(); // 再等 t2 执行完
        System.out.println("所有子线程执行完毕,主线程结束!");
    }
}
场景 2:并行等待(主线程同时等待多个已启动的子线程,子线程并行执行)
public class JoinParallelDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程 t1 执行中:i = " + i);
            }
        }, "线程 t1");

        Thread t2 = new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程 t2 执行中:i = " + i);
            }
        }, "线程 t2");

        // 先同时启动两个子线程
        t1.start();
        t2.start();

        // 主线程同时等待两个子线程(谁先执行完谁先唤醒,最终等待耗时为较慢线程的执行时间)
        t1.join();
        t2.join();
        System.out.println("所有子线程执行完毕,主线程结束!");
    }
}

五、关键注意事项(避坑指南)

  1. 异常必须处理:join() 抛出的 InterruptedException 是受检异常,若不捕获 / 声明抛出,编译报错。当调用线程被中断时,会触发该异常并清除中断状态。
  2. join() 不改变线程的启动状态:join() 仅负责阻塞调用线程,不会自动启动目标线程,必须先调用 targetThread.start(),再调用 targetThread.join()。
  3. 多个线程等待同一个目标线程:若多个线程同时调用同一个目标线程的 join() 方法,当目标线程终止时,所有等待线程会被 JVM 同时唤醒(底层 notifyAll())。
  4. 与锁结合时的释放特性:若调用线程持有某个对象锁,调用 join() 后会释放该锁(因为底层调用 wait()),而 sleep() 不会释放锁,这是并发编程中的关键差异。

调用对象必须是已启动的线程:若对未启动(NEW 状态)的线程调用 join(),方法会直接返回,不会产生阻塞(因为 isAlive() 返回 false,无需等待)。

Thread t = new Thread(() -> {});
t.join(); // 无阻塞,直接执行后续代码(t 未启动)

六、底层源码简化解析(帮助理解)

以下是 Thread.join() 的核心源码(JDK8),剔除了冗余逻辑,保留核心流程:

// 无参 join() 等价于 join(0)
public final void join() throws InterruptedException {
    join(0);
}

// 带毫秒参数的核心实现
public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    // millis=0 表示无限等待
    if (millis == 0) {
        // 只要目标线程还存活,调用线程就一直 wait
        while (isAlive()) {
            wait(0);
        }
    } else {
        // 超时等待逻辑:循环判断,避免虚假唤醒
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
源码关键点
  1. join() 是 synchronized 方法:锁对象是目标线程实例(保证线程安全)。
  2. 循环判断 isAlive():避免「虚假唤醒」(即使被意外唤醒,若目标线程未终止,仍会继续等待)。
  3. 依赖 Object.wait():底层是 Object 的等待 / 唤醒机制,JVM 在目标线程终止时自动调用 notifyAll()。

七、总结

  1. 核心功能:Thread.join() 实现线程同步,让调用线程阻塞等待目标线程终止或超时。
  2. 核心重载:无参(无限等待)、join(long millis)(毫秒超时)、join(long millis, int nanos)(高精度超时,极少使用)。
  3. 关键差异:与 sleep() 的核心区别是「释放对象锁」和「使用场景」,join() 用于线程同步,sleep() 用于当前线程暂停。
  4. 避坑要点:先启动目标线程再调用 join()、处理 InterruptedException、区分串行 / 并行等待。
  5. 底层逻辑:基于 Object.wait() 实现,通过循环判断 isAlive() 避免虚假唤醒,目标线程终止时由 JVM 唤醒等待线程。

目录

  1. 一、核心定位
  2. 二、重载方法
  3. 关键说明
  4. 三、工作原理
  5. 1. 核心逻辑
  6. 2. 与 Thread.sleep() 的核心区别
  7. 四、实战示例
  8. 示例 1:无参 join() - 主线程等待子线程完全执行
  9. 运行结果(顺序固定)
  10. 示例 2:带超时 join(long millis) - 主线程等待超时后继续执行
  11. 运行结果(主线程超时后提前执行)
  12. 示例 3:多个线程 join() - 串行等待 vs 并行等待
  13. 场景 1:串行等待(主线程依次等待多个子线程,子线程串行执行)
  14. 场景 2:并行等待(主线程同时等待多个已启动的子线程,子线程并行执行)
  15. 五、关键注意事项(避坑指南)
  16. 六、底层源码简化解析(帮助理解)
  17. 源码关键点
  18. 七、总结

更多推荐文章

查看全部
  • 基于 Axios 的前端文件上传下载实战指南
  • HTTP 状态码详解:从 200 到 500 的常见含义
  • Python 基于 Transformer 的时序数据建模与实现详解
  • 12 款 AI 写小说软件测评:大纲生成与正文创作工具对比
  • 计算机视觉技术与应用实践
  • C++ 与 Qt 开发环境搭建及 Windows 快捷键指南
  • Tauri 2.0 桌面开发实战:基于 Rust 与 Vue3 构建轻量级股票监控工具
  • Python 3.8+ 海象运算符详解
  • GitHub Copilot 实战:AI 编程助手提升开发效率指南
  • 日程事件管理日历应用(纯前端实现,支持导出 Excel)
  • Spring Boot Web 三大核心交互案例详解
  • 滑动窗口算法实战:水果成篮与异位词
  • C++ 类与对象全面剖析:构造函数深化与静态成员特性
  • 第十四届蓝桥杯 C/C++ 省赛 B 组题解
  • Web 应用架构与常见安全漏洞解析
  • 基于 LLaMA-Factory 的 LLM DPO 训练实战
  • 利用腾讯云 HAI 与 DeepSeek 快速构建个人网页
  • F5 刷新时,浏览器前端究竟发生了什么?
  • Redis Hash 类型详解
  • Linux 进程通信核心原理与实战:匿名管道、命名管道、进程池

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online