Java并发—Join使用及原理

一、理解

阅读JDK Join方法注释如下:

Waits for this thread to die.//等待该线程死亡

所以其作用是 等待该线程死亡。简单理解就是,调用该方法的线程阻塞,直到被调用Join的线程死亡。

我们回忆一下,线程的5种状态,如下:

  • NEW:新建
  • RUNNABLE:运行中
  • BLOCKED:阻塞。等待锁,通常是通过synchronize
  • WAITING:等待。通常是调用Object.wait(),Thread.join()
  • TIMED_WAITING:时间等待
  • TERMINATED:死亡

等待该线程死亡从状态角度看,就是等待其他状态转入死亡状态。

二、使用

2.1 Main线程等待子线程

我们先展示一个基本的例子,主线程等待子线程完成后执行。

public static void main(String[] args) {
    Thread thread1 = new Thread(new Runnable() {
        public void run() {
            System.out.println("子线程执行开始");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程执行结束");
        }
    });
    try {
        System.out.println("主线程执行开始");
        thread1.start();
        thread1.join();
        System.out.println("主线程执行完成");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2.2 T1、T2、T3依次等待

我们先构造T1、T2、T3线程,让它们分别等待6s、4s、2s。如果没有Join,执行完成顺序是T3、T2、T1,存在Join,则完成顺序为T1、T2、T3。

public static void main(String[] args) {
    final Thread t1 = new Thread(new Runnable() {
        public void run() {
            System.out.println("T1子线程执行开始");
            try {
                //设置6秒,让T1最后执行完
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T1子线程执行结束");
        }
    });
    final Thread t2 = new Thread(new Runnable() {
        public void run() {
            System.out.println("T2子线程执行开始");
            try {
                t1.join();
                //设置4秒
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T2子线程执行结束");
        }
    });
    Thread t3 = new Thread(new Runnable() {
        public void run() {
            System.out.println("T3子线程执行开始");
            try {
                t2.join();
                //设置2秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T3子线程执行结束");
        }
    });
    t1.start();
    t2.start();
    t3.start();
}

三、原理

Object.join()首先调用join(long millis)。代码如下:

public final void join() throws InterruptedException {
    join(0);//直接调用重载方法,入参为0,也就是等待最大时间为0,持续阻塞
}

下面是join的核心代码,我们在方法中逐一讲解

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();//获得初入方法时间
    long now = 0;//设置已经执行的时间,初始化为0

    if (millis < 0) {//验证参数合法性,小于0不合法
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {//等于0,说明无最大超时时间
        while (isAlive()) {//检查线程状态是否死亡,如果非死亡,则阻塞
            wait(0);//wait(0)等同于wait(),会一直阻塞。直到线程死亡时,会调用notifyAll()通知所有阻塞线程
        }
    } else {
        while (isAlive()) {检查线程状态是否死亡,如果非死亡,则执行
            long delay = millis - now; //计算剩余等待时间,通过最大等待时间-已经执行的时间
            if (delay <= 0) {//到达最大等待时间,则退出
                break;
            }
            wait(delay);//等待 剩余时长
            now = System.currentTimeMillis() - base;//每次循环执行,都会当前执行时间-初入方法时间,得到已经执行的时间
        }
    }
}

可以看到其核心就是检查Thread是否死亡状态,并通过wait()方法持续阻塞,直到线程死亡时,触发notifyAll。所以join的阻塞实际上也是通过native wait()方法实现的。

思考一

学习上面知识后,思考wait(0)即可实现一直等待,直到线程死亡,为什么外层需要while检查状态,而不使用if?

回答:因为目标Thread对象可能被其他线程持有,它们有可能调用notify()、notifyAll()接口,如果未使用while,会导致wait(0)被唤醒,join继续执行,而目标Thread仍未死亡。

你可能感兴趣的:(Java,Java后端技术)