难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
Java
开发环境
- JDK v11
- IntelliJ IDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.孔融让梨
前面在《“全栈2019”Java多线程第五章:线程睡眠sleep()方法详解》一章中讲了如何暂时停止执行线程,在《“全栈2019”Java多线程第六章:中断线程interrupt()方法详解》一章中又讲了如何停止线程,在 《“全栈2019”Java多线程第七章:等待线程死亡join()方法详解》一章中讲了如何让一个线程等待另一个线程执行完毕再执行。现在我们来讲解如何让一个线程放弃执行权。
在讲解今天的内容之前,我们先来看一个故事:
孔融四岁的时候,和哥哥们一起吃梨,孔融总是拿小的吃。有大人问他为什么这么做。他回答说:"我年龄小,食量小,按道理应该拿小的。“由于孔融这么聪明早慧,很小就懂得这样的道理,宗族亲戚们认为他是个奇才。
孔融让梨的故事告诉我们凡事应该懂得谦让的礼仪。
线程之间它们互相可以谦让吗?
可以。
那它们之间通过什么方式谦让呢?
一个线程可以放弃执行权,尽量将执行权给其他线程。
执行权?什么东西?
我们来看一张图:
每一个程序都有机会获得执行权,得到执行权的程序就可以被CPU执行。
如果一个程序获得了执行权但是它放弃了,那么其他程序就有可能得到执行权,这就是程序与程序之间礼让的表现方式。
在《“全栈2019”Java多线程第一章:认识多线程》一章中,我们知道进程就是一个独立运行的程序,例如上面的QQ、网易云音乐和迅雷。所以,程序与程序之间礼让就是进程与进程之间礼让。我们还知道每一个进程中至少有一个线程,所以进程之间礼让就是线程与线程之间的礼让。
那么,线程之间具体是通过什么方式礼让的呢?
线程之间的礼让体现在:通过调用yield()方法主动放弃线程执行权。
2.线程接力join()方法
在《“全栈2019”Java多线程第七章:等待线程死亡join()方法详解》一章中,我们学习了线程接力,就是一个线程等待另外一个线程执行完毕后再执行。
下面,我们来重温一下join()方法。
为什么要重温join()方法?
因为待会要用yield()方法来替代join()方法,也能达到让一个线程等待另外一个线程执行完毕后再执行,所以需要重温join()方法。
接下来,我会定义一个任务A线程:
再定义一个任务B线程:
任务A线程要执行的任务很简单,数数,从0数到999:
任务B线程要执行的任务也很简单,也是数数,从1000数到1999:
但是,有一个要求,任务B线程要等到任务A线程数完才可以开始数,也就是任务B线程要等到任务A执行完才可以开始执行,所以任务A线程需要在任务B线程中调用自己的join()方法。
我们先来看看没有使用join()方法时程序执行是怎么样的,书写我们的Main类:
在main()方法里面首先创建了TaskAThread线程对象和TaskBThread线程对象,紧接着,将两个线程对象启动起来。
运行程序,执行结果:
动图太快了,来看静图:
从运行结果来看,两个线程同时开始运行,数字的顺序也是杂乱无章的。
下面join()方法登场。
任务A线程需要在任务B线程中调用自己的join()方法:
那么,这个任务A线程对象怎么得到呢?可以通过任务B线程构造方法传进来:
此次改动有四处需要讲解:
首先是我们在TaskBThread类里面定义了一个有参构造方法,参数类型是Thread类型,目的就是将被等待的线程传递进来,然后在run()方法里面调用其join()方法:
其次是我们在TaskBThread类里面定义了一个成员变量来接收这个被等待的线程对象:
我们知道当类中只要显式地定义一个有参构造方法后,系统默认提供的无参构造方法就不存在了,所以这里我们为了保证无参构造方法还能用,又定义了一个无参的构造方法:
最后我们在run()方法里面调用了被等待线程的join()方法,使其TaskBThread线程在被等待线程执行完毕后执行:
在调用join方法之前,我们还对被等待线程进行判空:
好了,TaskBThread类改写完成。
下面我们来改写Main类:
然后,我们运行程序,执行结果:
动图太快?
我们来把数数的范围调小一点,TaskAThread线程从0数到99:
TaskBThread线程从100数到199:
再运行程序,执行结果:
动图太快?我们来个慢点的(时长:19秒):
从运行结果来看,符合我们预期。
接下来,我们今天的主角yield()方法要登场了,使用yield()方法替代我们的join()方法到达让一个线程先执行完,然后再执行自己的效果。
3.放弃执行权yield()方法
yield()方法在我们Thread类中的源码:
将注释翻译成中文:
去掉注释版:
yield()方法作用是使当前线程放弃执行权。
访问权限
public:yield()方法访问权限是公开的。
static:yield()方法是静态方法。类方法。
native:yield()方法具体是用本地方法实现的。
void:yield()方法没有返回值。
yield()方法可以直接使用类名调用。
注:
native关键字:用来声明一个方法是由与计算机相关的语言(如C/C++/FORTRAN语言)实现的。
参数
无。
抛出的异常
无。
应用
接下来,我们来用yield()方法改写之前的程序。在改写之前,捋一下程序执行顺序,TaskBThread线程在TaskAThread线程执行完毕后执行。所以现在要做的就是:只要在TaskAThread线程没有执行完毕之前,TaskBThread线程放弃执行权就好了。
于是,我们就按照这个思路来改写之前的程序。
在改写之前,我有一个问题要问下大家:TaskBThread线程它怎么知道TaskAThread线程什么时候执行完?
TaskBThread线程现在是无法知道TaskAThread线程什么时候执行完的,如果TaskAThread线程有一个执行完毕的标记就好了,我们TaskBThread线程就可以根据TaskAThread线程的这个标记来判断TaskAThread线程执行完没有。
好,我们来给TaskAThread线程类中定义一个记录线程执行完毕的标记:
接着,我们要在任务执行完毕后面将任务是否执行完成标记设为已完成,也就是true:
TaskAThread线程类改写完毕,接下来,我们去改写TaskBThread线程类。
TaskBThread线程类里面要做的就是根据TaskAThread线程类里面的任务是否执行完毕标记来判断是否需要放弃执行权,让TaskAThread线程先执行完。
于是,我们就按照这个思路来改写TaskBThread线程类:
此次改动有三处:
首先,我们将有参构造方法里面的参数类型改为了TaskAThread:
为什么要改为TaskAThread呢?
因为这里不能再使用公共父类Thread了,父类Thread里面没有任务是否完成标记completed字段,所以这里得明确类型。
接着,既然接收类型改了,那么我们成员变量的类型也得改:
最后,我们在run()方法里面就要判断TaskAThread线程里面的任务是否执行完毕标记来执行对应的操作,比如放弃执行权:
好了,TaskAThread类和TaskBThread类都改好了。
Main类不用修改,所以我们可以运行程序,执行结果:
从运行结果来看,并不符合我们预期。
我们可以从结果中看到,这里顺序是乱的:
yield()方法用错了吗?
没有。
那结果没有对?
因为我们只放弃了一次执行权,后面还是有可能再获得执行权,后面的执行权我们都没有放弃,所以才导致这样的结果。
那需要怎样做?
我们需要在TaskBThread线程类的run()方法里面用while循环判断TaskAThread线程类里面的任务是否执行完成标记:
if语句只判断一次;而while语句是判断多次。
这样当我们TaskAThread线程里面的任务执行完成时,我们while循环条件也就变为false,这样就不用再放弃执行权了,开始执行TaskBThread线程任务。
好了,我们再次运行程序,执行结果:
动图太快?我们来个慢点的(时间:19秒):
从运行结果来看,符合我们预期。
4.yield()方法应用
yield()方法更多的是应用在多个线程间相互配合。
比如上述程序中,TaskAThread线程与TaskBThread线程之间的配合,TaskBThread线程等到TaskAThread线程任务执行完成后再执行,在等待期间,TaskBThread一直都是在放弃执行权,使其能够给TaskAThread线程更多执行权。
诸如类似的程序有很多,所以大家只需记住yield()方法两点即可:放弃执行权和线程间配合。
5.拼命都要记住的yield()方法注意点
这里要说一个和sleep()方法类似的点,就是yield()方法和sleep()方法一样,针对的都是当前线程,千万不要想着用哪个线程对象调用yield()方法或sleep()方法就是作用在该线程对象上!!!
你比如:
这里不是让TaskAThread线程放弃执行权,这是在让主线程放弃执行权!!!
重要的事说三遍:
yield()方法作用是使当前线程放弃执行权。
yield()方法作用是使当前线程放弃执行权。
yield()方法作用是使当前线程放弃执行权。
千万要注意!
千万要注意!
千万要注意!
总结
- 一个线程可以放弃执行权,尽量将执行权给其他线程。
- 线程之间的礼让体现在:通过调用yield()方法主动放弃线程执行权。
- yield()方法作用是使当前线程放弃执行权。
- yield()方法和sleep()方法一样,针对的都是当前线程,千万不要想着用哪个线程对象调用yield()方法或sleep()方法就是作用在该线程对象上!!!
至此,Java中放弃执行权yield()方法相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
“全栈2019”Java多线程第七章:等待线程死亡join()方法详解
下一章
“全栈2019”Java多线程第九章:判断线程是否存活isAlive()详解
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
版权声明
原创不易,未经允许不得转载!