java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java Thread.join()方法

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

作者:码灵

Thread.join()是Java多线程编程中的关键方法,用于确保线程执行顺序和数据完整性,下面这篇文章主要介绍了Java Thread.join()方法使用的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

全面介绍Java 中Thread.join()方法的用法,包括其核心作用、重载方法、工作原理、使用场景及注意事项,从基础到进阶逐步拆解,结合可运行示例帮你彻底理解。

一、核心定位

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 会自动调用T2notifyAll()方法,唤醒所有等待在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("主线程继续执行,程序结束!");
    }
}

运行结果(顺序固定)

plaintext

主线程启动子线程...
主线程开始等待子线程执行完毕...
子线程执行中: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("主线程等待超时,继续执行自身逻辑!");
    }
}

运行结果(主线程超时后提前执行)

plaintext

主线程启动子线程...
主线程开始等待子线程(超时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. 调用对象必须是已启动的线程:若对未启动(NEW 状态)的线程调用join(),方法会直接返回,不会产生阻塞(因为isAlive()返回 false,无需等待)。
    Thread t = new Thread(() -> {});
    t.join(); // 无阻塞,直接执行后续代码(t未启动)
    
  2. 异常必须处理join()抛出的InterruptedException是受检异常,若不捕获 / 声明抛出,编译报错。当调用线程被中断时,会触发该异常并清除中断状态。
  3. join()不改变线程的启动状态join()仅负责阻塞调用线程,不会自动启动目标线程,必须先调用targetThread.start(),再调用targetThread.join()
  4. 多个线程等待同一个目标线程:若多个线程同时调用同一个目标线程的join()方法,当目标线程终止时,所有等待线程会被 JVM 同时唤醒(底层notifyAll())。
  5. 与锁结合时的释放特性:若调用线程持有某个对象锁,调用join()后会释放该锁(因为底层调用wait()),而sleep()不会释放锁,这是并发编程中的关键差异。

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

以下是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 唤醒等待线程。

到此这篇关于Java Thread.join()方法使用的文章就介绍到这了,更多相关Java Thread.join()方法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文