难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
java
开发环境
- JDK v11
- IntelliJ IDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.接力
前面在 一章中讲了 如何暂时停止执行线程 ,在 一章中又讲了 如何停止线程 ,现在我们来讲解 如何让一个线程等待另一个线程执行完毕再执行 。
在讲解今天的内容之前,我们先来看两个常见的体育运动:

短跑是大家 同时起跑 ;接力是 队友一个接一个的跑 。
为什么要介绍这两个运动?
目的就是要 让线程跟接力一样,一个接着一个运行 。
现在我们的线程一旦被创建启动之后,他就自己执行去了。跟其他线程没有关系,也不存在谁等谁的问题,更不要说谁要等谁执行完毕之后再执行。
2.现阶段线程执行现状
我们现在来创建3个线程,创建并启动它们。看看它们执行的顺序是怎样的。
演示:
请创建3个线程并启动它们。
请观察程序代码及结果。
代码:
Main类:

结果:

从运行结果来看,符合我们预期。
为什么说符合预期?
因为我们创建了三个线程:

然后,这三个线程都被睡眠不同时间,thread1被睡了3秒钟:

thread2被睡了2秒钟:

thread1被睡了1秒钟:

然后,它们的启动顺序是thread1>thread2>thread3。

也就是说thread1最先启动,睡眠时间最长,thread2其次,thread3最后启动,睡眠时间最短。按理说,打印输出顺序就是thread3里面的执行内容、thread2里面的执行内容和thread1里面的执行内容。

和我们最后的结果一致。
现在,我们需要的不是这样一种结果。我们需要的是先让thread1执行完,然后是thread2执行,最后是thread3执行完。
有小伙伴版就说,那还不简单,直接让最先执行完的那个线程睡眠时间最短,后面的执行的线程依次往后累加线程睡眠时间。
这个办法不靠谱,为什么呢?
因为每个线程的执行内容不一样,导致执行这个内容的时间也不一致,所以设置线程睡眠时间来给线程执行排序是不靠谱的。
哪怎么办呢?
如果让线程跟接力一样,一个接着一个运行,后一个等前一个执行完毕后再执行。这样问题不就解决了!
那这个办法怎么实现?
可以使用Thread类的join()方法来实现,具体实现请看第3小节。
3.等待线程执行完毕join()方法
join()方法在Thread类中有三个重载方法:
- void join()
- void join(long millis)
- void join(long millis, int nanos)
这三个join()方法的源码:

将注释翻译成中文:

去掉注释版:

可能大家还是看不清,不过没关系,下面会一个一个讲解。
首先,这一小节先讲解的是join()方法。

join()方法的作用就是等待当前线程死亡。
线程死亡 是什么意思?
线程死亡就是run()方法返回,线程执行完毕 。
访问权限
public :join()方法访问权限是公开的。
final :join()方法是最终方法。无法被子类重写。
void :join()方法没有返回值。
参数
无。
抛出的异常
throws InterruptedException :当有任何线程中断了当前线程时,就会抛出此异常。InterruptedException中的Interrupted中文有打断、中断的意思。抛出此异常时,将清除当前线程的中断状态。
应用
前面说到join()方法可以让线程一个接着一个运行。这里我们就来试试。
使用join()方法改写我们的Main类:

运行程序,执行结果:

从运行结果来看,符合我们预期。
的确是线程thread2等待线程thread1执行完毕后再执行,线程thread3等待线程thread2执行完毕后再执行的。
注意
需要注意的是,join()方法在线程中断情况下会产生InterruptedException异常。
例如,我们将thread3中断:

运行程序,执行结果:

静图:

错误信息:

文字版:
从运行结果来看,我们的线程thread1和thread2都正常执行了,但是这个 线程thread3因为线程中断被抛异常了 。
为什么呢?
线程只有在阻塞的时候被中断才会抛出InterruptedException异常,其他时候基本上不会抛出InterruptedException异常。所以,我们断定join()方法里面的等待另外一个线程执行完毕的方法肯定跟线程阻塞相关。
果不其然,在异常信息中我们就看到类似的代码:

wait()方法的作用就是让线程等待。无论是让线程睡眠还是让线程等待都是让线程阻塞,所以证实的是join()方法里面就有调用wait()方法的痕迹,至于wait()方法详解,后续章节马上讲解到,希望大家可以持续关注。
4.设置等待最大时间join(long millis)方法
前面小节写的线程等待的例子中, 所有等待执行的线程都好像无期限的永远的等下去,直到前面一个线程执行完毕 。
如果前面一个线程睡眠了N毫秒,一时半会醒不来呢?等待的线程是不是还得干等着?
照前面使用join()方法的写法,你只能干等着。
有没有一个办法能让等待线程等一会之后,如果前面的线程仍然没有执行完毕,我这个等待的线程就不等了,开始执行任务?
可以,这就是join()方法的重载方法join(long millis)方法。
join(long millis)方法在Thread类中的源码:

将注释翻译成中文:

去掉注释版:

join(long millis)方法方法的作用是等待当前线程死亡,用户可以指定最大等待时间。等待时间为0意味着永远等待。
访问权限
public :join(long millis)方法访问权限是公开的。
final :join(long millis)方法是最终方法。无法被子类重写。
synchronized :join(long millis)方法是同步方法。线程安全。
void :join(long millis)方法没有返回值。
注:synchronized是一个关键字,暂时不理解的小伙伴可以不用管它,后续章节会有讲解。
参数
millis :等待的时间(单位:毫秒)。
抛出的异常
throws IllegalArgumentException :当millis值为负数时,就会产生IllegalArgumentException异常。
throws InterruptedException :当有任何线程中断了当前线程时,就会抛出此异常。InterruptedException中的Interrupted中文有打断、中断的意思。抛出此异常时,将清除当前线程的中断状态。
应用
接下来,我们就来试试join(long millis)方法。
使用join(long millis)方法改写我们Main类:

具体改写的地方就是在线程thread2等待线程thread1这里,让线程thread2最大等待线程thread1执行完毕时间为1000毫秒(即1秒),否则线程thread2将不必等待线程thread1执行完毕后再执行,最大等待时间一过则可执行。
运行程序,执行结果:

从运行结果来看,符合我们预期。
如果大家觉得动图刷新太快的话,可以看看静图:

这里就充分展示了join(long millis)方法的用法。
5.无限等待
这里我们说一个join(long millis)方法的特殊值:0。
为什么说0是一个特殊值呢?
因为 等待时间为0意味着永远等待。
永远等待的意思是我永远等待前面一个线程执行完毕之后我再执行。
我们的 join()方法内部就是调用的join(long millis)方法,然后参数millis传入的是0 :

这里是需要大家注意的地方。
6.设置最大等待时间join(long millis, int nanos)方法
join()方法的重载方法还有最后一个: join(long millis, int nanos)方法 。
join(long millis, int nanos)方法比join(long millis)方法多了一个参数nanos,nanos是 纳秒 ,给millis附加的,但是它并不是真正加上去的,而是 当我们nanos取值大于500000就可以促使millis++(即millis累加1毫秒) 。
这一特点我们在 一章中也详细讲解过。
join(long millis, int nanos)方法在Thread类中的源码:

将注释翻译成中文:

去掉注释版:

join(long millis, int nanos)方法方法的作用是等待当前线程死亡,用户可以指定最大等待时间。等待时间为0意味着永远等待。
访问权限
public :join(long millis, int nanos)方法访问权限是公开的。
final :join(long millis, int nanos)方法是最终方法。无法被子类重写。
synchronized :join(long millis, int nanos)方法是同步方法。线程安全。
void :join(long millis, int nanos)方法没有返回值。
注:synchronized是一个关键字,暂时不理解的小伙伴可以不用管它,后续章节会有讲解。
参数
millis :等待的时间(单位:毫秒)。
nanos :附加给millis的纳秒值(单位:纳秒)。取值范围在0-999999之间。
抛出的异常
throws IllegalArgumentException :当millis值为负数或nanos值不在0-999999之间时,就会产生IllegalArgumentException异常。
throws InterruptedException :当有任何线程中断了当前线程时,就会抛出此异常。InterruptedException中的Interrupted中文有打断、中断的意思。抛出此异常时,将清除当前线程的中断状态。
应用
接下来,我们就来试试join(long millis, int nanos)方法。
使用join(long millis, int nanos)方法改写我们Main类:

改写的地方是millis传入的999毫秒,nanos传入的是500000纳秒。
因为nanos>=500000时,millis++(即millis加1毫秒),所以999毫秒+1毫秒=1000毫秒,即1秒。线程thread2最大等待时间为1000毫秒。
运行程序,执行结果:

从运行结果来看,符合我们预期。
动图看不清的小伙伴,可以看静图:

join(long millis, int nanos)方法演示到此为止,其他注意事项都和join(long millis)方法一致,这里就不再赘述。
总结
- join()方法的作用就是等待当前线程死亡。
- 线程死亡就是run()方法返回,线程执行完毕。
- join(long millis)方法方法的作用是等待当前线程死亡,用户可以指定最大等待时间。等待时间为0意味着永远等待。
- 等待时间为0意味着永远等待。
- 永远等待的意思是我永远等待前面一个线程执行完毕之后我再执行。
- join()方法内部就是调用的join(long millis)方法,然后参数millis传入的是0。
- join(long millis, int nanos)方法方法的作用是等待当前线程死亡,用户可以指定最大等待时间。等待时间为0意味着永远等待。
至此,Java中等待线程join()方法相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
下一章
“全栈2019”Java多线程第八章:放弃执行权yield()方法详解
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。

版权声明
原创不易,未经允许不得转载!