理解join()方法之前请确保对wait()/notify()/notifyAll()机制已熟练掌握。

1. join()方法的作用是等待线程销毁。

join()方法反应的是一个很现实的问题,比如main线程的执行时间是1s,子线程的执行时间是10s,但是主线程依赖子线程执行完的结果,这时怎么办?可以像生产者/消费者模型一样,搞一个缓冲区,子线程执行完把数据放在缓冲区中,通知main线程,main线程去拿,这样就不会浪费main线程的时间了。

另外一种方法,就是join()了。

2. 例一:阻塞mian线程

先来看一段代码:

static int r = 0;
public static void main(String[] args) throws InterruptedException{
    test1();
}

private static void test1() throws InterruptedException {
    log.debug("开始");
    Thread t1 = new Thread(() -> {
        log.debug("开始");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("结束");
        r = 10;
    }, "t1");
    t1.start();
    // t1.join();
    log.debug("结果为:{}", r);
}

运行结果:可以看到在r的值并没有被修改就被打印了。

10:44:40.264 [main] DEBUG joinTest - 开始
10:44:40.273 [t1] DEBUG joinTest - 开始
10:44:40.272 [main] DEBUG joinTest - 结果为:0
10:44:40.375 [t1] DEBUG joinTest - 结束

我们取消对t1.join()的注释,其余不变,查看结果:

10:49:00.045 [main] DEBUG joinTest - 开始
10:49:00.053 [t1] DEBUG joinTest - 开始
10:49:00.162 [t1] DEBUG joinTest - 结束
10:49:00.163 [main] DEBUG joinTest - 结果为:10

意思是,join()方法会使调用join()方法的线程(也就是t1线程)所在的线程(也就是main线程)无限阻塞,直到调用join()方法的线程销毁为止,此例中main线程就会无限期阻塞直到t1的run()方法执行完毕

3. 例二:不能阻塞同级线程

如果大家对于上述的例子还有疑问:
比如有没有可能是2个并行线程,执行join方法阻塞另一个?

看一个例子:

public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("vip来了" + i + Thread.currentThread().getName());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestJoin testJoin = new TestJoin();
        Thread a = new Thread(testJoin, "a");
        Thread b = new Thread(testJoin, "b");
        a.start();
        b.start();
        a.join();
        System.out.println("我是main,我完了!!!");
    }
}

结果:

vip来了0b
vip来了0a
vip来了1b
vip来了1a
vip来了2b
vip来了2a
vip来了3b
vip来了3a
vip来了4a
vip来了4b
我是main,我完了!!!

如果按上述想法,很明显,应该等a执行完,再执行b,很明显结果并不是这样,结果是调用join方法的main线程在等a线程和b线程执行完成后,再执行。

在看一个例子,证明是调用方也就是父线程被阻塞:

4. 例三:阻塞调用方线程

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

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");
        }
    }
}
class AThread extends Thread {
    BThread bt;
    public AThread(BThread bt) {
        super("[AThread] Thread");
        this.bt = bt;
    }
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " start.");
        try {
            bt.join();
            System.out.println(threadName + " end.");
        } catch (Exception e) {
            System.out.println("Exception from " + threadName + ".run");
        }
    }
}

打印结果:

main start.
[BThread] Thread start. // 父线程B启动执行
[BThread] Thread loop at 0
[BThread] Thread loop at 1
[AThread] Thread start. // 2秒后main线程睡眠结束,启动at线程,at线程被bt.join阻塞
[BThread] Thread loop at 2
[BThread] Thread loop at 3
[BThread] Thread loop at 4
[BThread] Thread end.
[AThread] Thread end. // bt线程执行完,at线程开始执行
main end! // 由于调用了at.join,所以只能等待at线程执行完再执行at的父线程main线程

做一些修改,将上述的main方法中的at.join()注释,查看运行结果:

main start.
[BThread] Thread start. // bt线程启动
[BThread] Thread loop at 0
[BThread] Thread loop at 1
main end! // main线程由于没有被阻塞,所以和其他线程并行执行。此时main线程已经执行结束
[AThread] Thread start. // at线程启动,由于被bt.join阻塞,只能等bt线程执行完执行
[BThread] Thread loop at 2
[BThread] Thread loop at 3
[BThread] Thread loop at 4
[BThread] Thread end.
[AThread] Thread end.// at线程执行,

从这个例子可以看出来,阻塞的是调用join方法的线程的调用线程。

5. 例四:带参数来完成限时同步

join是可以带参数的:join(2000)也是可以的,表示调用join()方法的线程所在的线程最多等待2000ms,也就是比如main线程只会被阻塞2000毫秒

static int r1 = 0;
static int r2 = 0;

public static void main(String[] args) {
    test();
}

public static void test() {
    try {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
                r1 = 10;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Long start = System.currentTimeMillis();
        t1.start();

        log.debug("join begin");
        // 表示主线程只会等待1500毫秒
        t1.join(1500);
        Long end = System.currentTimeMillis();
        log.debug("r1:{} r2: {} cost:{}", r1,r2,end-start);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

结果:

21:44:01.942 [main] DEBUG com.multiThread.TestJoin - join begin
# 可以看到r1的结果还是0,因为在1500毫秒的时候主线程就已经执行了输出语句了
21:44:03.445 [main] DEBUG com.multiThread.TestJoin - r1:0 r2: 0 cost:1506

如果将上面的join时间改为3000毫秒,来看看执行结果:

21:47:50.881 [main] DEBUG com.multiThread.TestJoin - join begin
# 可以看到,r1的结果是10,被阻塞到调用join方法的线程执行完成后立即执行,
# 并不会等到3秒后再执行
21:47:52.879 [main] DEBUG com.multiThread.TestJoin - r1:10 r2: 0 cost:2000

6. 例五:主线程等待多个线程的运行结果:

static int r1 = 0;
static int r2 = 0;
private static void test2() throws InterruptedException {
    log.debug("开始");
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        r1 = 10;
    }, "t1");
    Thread t2 = new Thread(() -> {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        r2 = 20;
    }, "t1");
    t1.start();
    t2.start();
    long start = System.currentTimeMillis();
    log.debug("join begin");
    t1.join();
    log.debug("t1 join end");
    t2.join();
    log.debug("t2 join end");
    long end = System.currentTimeMillis();
    log.debug("r1:{} r2:{} cost:{}" , r1, r2, end - start);
}

结果:

11:45:28.365 [main] DEBUG joinTest - 开始
11:45:28.373 [main] DEBUG joinTest - join begin
11:45:28.474 [main] DEBUG joinTest - t1 join end
11:45:28.579 [main] DEBUG joinTest - t2 join end
11:45:28.579 [main] DEBUG joinTest - r110 r2:20 cost:206

如果吧t1.join和t2.join的顺序调换一下,结果:

11:54:52.242 [main] DEBUG joinTest - 开始
11:54:52.247 [main] DEBUG joinTest - join begin
11:54:52.450 [main] DEBUG joinTest - t1 join end
11:54:52.450 [main] DEBUG joinTest - t2 join end
11:54:52.450 [main] DEBUG joinTest - r1:10 r2:20 cost:203

原理示例图:

java 多线程 juc java 多线程 join 慢_主线程

7. 和sleep的区别:

join()方法的一个重点是要区分出和sleep()方法的区别。。
两者的区别在于:sleep(2000)不释放锁,join(2000)释放锁,因为join()方法内部使用的是wait(),因此会释放锁。看一下join(2000)的源码就知道了,join()其实和join(2000)一样,无非是join(0)而已:

public final synchronized void join(final long millis) throws InterruptedException {
    if (millis > 0) {
        if (isAlive()) {
            final long startTime = System.nanoTime();
            long delay = millis;
            do {
                wait(delay);
            } while (isAlive() && (delay = millis -
                    TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
        }
    } else if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        throw new IllegalArgumentException("timeout value is negative");
    }
}

8. join与异常

如果join过程中,当前线程对象被中断,则出现线程出现异常