Thread join()的使用场景和原理

1、使用场景

  一般情况下,主线程创建并启动子线程,如果子线程中执行大量耗时运算,主线程可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束。主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景。

2、join()方法定义和使用

  join()是Thread类的一个方法。根据jdk文档的定义:

  public final void join()throws InterruptedException: Waits for this thread to die.

  join()方法的作用,是等待这个线程结束;这个定义比较模糊,个人认为"Java 7 Concurrency Cookbook"的定义较为清晰:

Waiting for the finalization of a thread

In some situations, we will have to wait for the finalization of a thread. For example, we may have a program that will begin initializing the resources it needs before proceeding with the rest of the execution. 
We can run the initialization tasks as threads and wait for its finalization before continuing with the rest of the program.
For this purpose, we can use the join() method of the Thread class. When we call this method using a thread object, it suspends the execution of the calling thread until the object called finishes its execution.

  解释一下,是主线程等待子线程的终止。也就是说主线程的代码块中,如果碰到了t.join()方法,此时主线程需要等待(阻塞),等待子线程结束了(Waits for this thread to die.),才能继续执行t.join()之后的代码块。

                                      

   来看一个join()的案例demo:

public class JoinDemo {

    public static void main(String[] args) throws InterruptedException {
        //获取当前线程信息
       Thread previousThread= Thread.currentThread();
        for(int i=0;i<10;i++){
            Thread thread=new Thread(new Domino(previousThread));
            thread.start();
            previousThread=thread;
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println("Thread.currentThread().getName()+\" terminate.\" = " + Thread.currentThread().getName()+" terminate.");

    }

    static class Domino implements Runnable{
        private Thread thread;
        public Domino(Thread thread){
            this.thread=thread;
        }
        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }
}

  这段demo的逻辑就是当前线程等待它前面的线程执行结束再执行,输出如下:

Thread.currentThread().getName()+" terminate." = main terminate.
Thread-0 terminate
Thread-1 terminate
Thread-2 terminate
Thread-3 terminate
Thread-4 terminate
Thread-5 terminate
Thread-6 terminate
Thread-7 terminate
Thread-8 terminate
Thread-9 terminate

3、源码分析

public final void join() throws InterruptedException {
    join(0);
}

  再看join(long millis)方法:

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");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

  isAlive() 是 Thread 内部的一个 native 方法,只要当线程处于 NEW 和 TERMINATED 状态时返回false,其余都返回true。join() 其实就是通过调用 wait() 方法(Object定义的,详见线程间通信),wait(0) 表示当前线程立即释放锁(这里的锁指的就是前一个线程)并进入 waiting 状态,等待其他线程唤醒(notify/notifyAll)。

  join() 一共有三个重载版本,分别是无参、一个参数、两个参数:

public final void join() throws InterruptedException;

 public final synchronized void join(long millis) throws InterruptedException;
 
 public final synchronized void join(long millis, int nanos) throws InterruptedException;

  (1) 三个方法都被final修饰,无法被子类重写。

  (2) join(long), join(long, long) 是synchronized method,同步的对象是当前线程实例。

  (3) join() 和 join(0) 是等价的,表示一直等下去;join(非0)表示等待一段时间。

  从源码可以看到 join(0) 调用了Object.wait(0),其中Object.wait(0) 会一直等待,直到被notify/中断才返回。

  while(isAlive())是为了防止子线程伪唤醒(spurious wakeup),只要子线程没有TERMINATED的,父线程就需要继续等下去。

  (4) join() 和 sleep() 一样,可以被中断(被中断时,会抛出 InterrupptedException 异常);不同的是,join() 内部调用了 wait(),会出让锁,而 sleep() 会一直保持锁。

4、join() 方法注意点

4.1、join() 与 start() 的调用顺序

package com.dxz.join;
class BThread extends Thread {
    public BThread() {
        super("[BThread] Thread");
    };
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " start.");
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println(threadName + " loop at " + i);
                Thread.sleep(1000);
            }
            System.out.println(threadName + " end.");
        } catch (Exception e) {
            System.out.println("Exception from " + threadName + ".run");
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " start.");
        BThread bt = new BThread();
        try {
            bt.join();
            bt.start();
            Thread.sleep(2000);
        } catch (Exception e) {
            System.out.println("Exception from main");
        }
        System.out.println(threadName + " end!");
    }
}

  执行结果:

main start.
[BThread] Thread start.
[BThread] Thread loop at 0
[BThread] Thread loop at 1
main end!
[BThread] Thread loop at 2
[BThread] Thread loop at 3
[BThread] Thread loop at 4
[BThread] Thread end.

  main线程没有等待 [BThread] 线程执行完再执行。join方法必须在线程start方法调用之后调用才有意义。这个也很容易理解:线程没有执行,isAlive() 方法就不会返回true,也就不会执行 wait(0) 方法了。

4.2、join() 与中断异常

  在join()过程中,如果当前线程被中断,则当前线程出现异常。(注意是调用thread.join()的线程被中断才会进入异常,比如a线程调用b.join(),a中断会报异常而b中断不会异常)

  下面总结下中断与 wait()/join()/sleep() 的使用:

  调用interrupt()方法,立刻改变的是中断状态,但如果不是在阻塞态,就不会抛出异常;如果在进入阻塞态后,中断状态为已中断,就会立刻抛出异常,但同时中断标志也会被系统复位。

(1)sleep() &interrupt()

  线程A正在使用sleep()暂停着: Thread.sleep(100000),如果要取消它的等待状态,可以在正在执行的线程里(比如这里是B)调用a.interrupt()[a是线程A对应到的Thread实例],令线程A放弃睡眠操作。即,在线程B中执行a.interrupt(),处于阻塞中的线程a将放弃睡眠操作。

  当在sleep中时线程被调用interrupt()时,就马上会放弃暂停的状态并抛出InterruptedException。抛出异常的,是A线程。

(2)wait() &interrupt()

  线程A调用了wait()进入了等待状态,也可以用interrupt()取消。不过这时候要注意锁定的问题。线程在进入等待区,会把锁定解除,当对等待中的线程调用interrupt()时,会先重新获取锁定,再抛出异常。在获取锁定之前,是无法抛出异常的。

(3)join() &interrupt()

  当线程以join()等待其他线程结束时,当它被调用interrupt(),它与sleep()时一样,会马上跳到catch块里.。

  注意,调用的interrupt()方法,一定是调用被阻塞线程的interrupt方法。如在线程a中调用线程t.interrupt()。

posted @ 2024-04-02 19:45  jingyi_up  阅读(777)  评论(0编辑  收藏  举报