亮剑 | 意大利炮轰 Java多线程join方法💯

4,674 阅读4分钟


有一天我走在路上,偶尔接触到了Thread.join()方法,便到网上去查阅相关资料,但也是看得我一头雾水(花露水)。在我很久的理解之后,便想将我理解的join()方法以一张图的方式解释出来。

一、我画

开局一张图:

我绘制的这张图以时间线的形式解释了主线程和各个被实例化的John线程的运行过程。

别着急,多看两遍! 我们可以看到,在主线程调用了join()方法后,指定的线程会被抓回来在后边老老实实地排队。

二、你猜

💡 问:请回答未执行join()方法和执行了join()方法的运行结果有什么不同(可能有几种结果、运行的时间、输出的顺序)?

🌱 答:

类型 可能结果数量 运行速度 输出顺序 时间复杂度(最好)
不使用join() 6种 快(多线程 乱序 O(1)
使用join() 1种 慢(单线程) 顺序 O(n)
  • 看完后,你可能还会有些疑问:

💡 Q:为什么不使用join()的可能结果数量是6种?输出顺序为什么是乱序?

🌱 A:由于多线程的原因,虽然start()顺序执行了线程1/2/3,但可能由于各种原因,某个线程会抢先完成,从而造成了6种运行结果 ——

123  
132  
213  
231  
321  
312
  • 多线程并不老实,并且难以(ye不是不可能)控制。

💡 Q:为什么使用join()后会变慢?

🌱 A:因为使用join()后,三个线程会按顺序被赶到主线程去运行,这时候它们就不能够同时运行了,只能一个一个地运行

三、动手

别偷懒,乘还年轻,打开你的IDE,把下边的代码粘贴进去:

public class TestJoin {
    public static void main(String[] args) {
        John john1 = new John();
        John john2 = new John();
        John john3 = new John();

        try {
            john1.start();
            john1.join();
            john2.start();
            john2.join();
            john3.start();
            john3.join();
        } catch (InterruptedException IE) {
            IE.printStackTrace();
        }
    }
}

class John extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 2; i++)
        {
            try
            {
                Thread.sleep(500);
                System.out.println("Current Thread: "
                        + Thread.currentThread().getName());
            }catch(Exception ex){
                System.out.println("Exception has" +
                        " been caught" + ex);
            }
            System.out.println(i);
        }
    }
}

运行结果:

Current Thread: Thread-0
0
Current Thread: Thread-0
1
Current Thread: Thread-1
0
Current Thread: Thread-1
1
Current Thread: Thread-2
0
Current Thread: Thread-2
1
  • 可以看到,苦逼的三个线程都被拉到了主线程顺序执行。

现在,删掉两行代码:

john2.join();
john3.join();

再次运行:

Current Thread: Thread-0
0
Current Thread: Thread-0
1
Current Thread: Thread-1
0
Current Thread: Thread-2
0
Current Thread: Thread-2
1
Current Thread: Thread-1
1
  • 可以看到,john2和john3两个线程并未在主线程中运行,所以运行结果也发挥得比较自由,且运行时间也缩短了。

此处请注意,在使用join()方法之前,一定要先使用start()方法启动线程。线程根本没工作,那还咋拉过去?

源码分析:

 /**
     * Waits for this thread to die.
     * 等待该线程死亡
     */
    public final void join() throws InterruptedException {
        join(0);
    }
    
     /**
     * Waits at most {@code millis} milliseconds for this thread to die
     * 最多等待code毫秒为该线程死亡,就是说如果该线程死亡时间小于该code则提前结束,继续往下执行
     * 如果该线程在code毫秒内没有死亡,到时间了则不管该线程,继续往下执行
     *. A timeout of {@code 0} means to wait forever.
     * 如果时间code=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");
        }

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

源码中:

  1. 就是说join()=join(0)
  2. 如果时间code millis=0的时候是无限等待,直到该线程消亡
  3. 如果时间是大于0的则该线程执行时间等于该时间后无论是否消亡,都将通知调用join方法的线程去继续执行,即notifyAll
public static void main(String[] args) {
        John john1 = new John();
        John john2 = new John();
        John john3 = new John();

        try {
            john1.start();
            john1.join();
            john2.start();
            john2.join(5);
            john3.start();
            john3.join();
        } catch (InterruptedException IE) {
            IE.printStackTrace();
        }
    }

执行结果

Current Thread: Thread-0
0
Current Thread: Thread-0
1
Current Thread: Thread-1
0
Current Thread: Thread-2
0
Current Thread: Thread-1
1
Current Thread: Thread-2
1
  • 可以看到,线程john2在线程被主线程加入进来5毫秒之后,线程3启动,并且开始执行,join()的用处其实就是线程的协同,线程的有序执行,线程的节制于时间的调度~

四、结束

到此,你已经掌握了join()方法的使用。不要问我能用来做什么,等到你需要它的功能时,你就不会再手忙脚乱了。

很认真,你读到了最后。让我们再来讲讲yield()方法吧(别的文章都讲了):

yield()方法的作用,与join()方法无瓜。当你对一个线程执行yield()方法后,该线程会尝试暂停自己,给后面正在排队的同级线程让道(即拱手让人),它是一个不稳定的过程(不一定有效果,也不一定什么时候继续执行)。

另外,建议你趁着还能再学进点什么,再看看syncronized、wait()、notify()这些有相似之处的知识比较好。

看图谱我都给你找来了: