(多线程)等待一个线程-join() 获取当前线程的引用 线程的六种状态 线程休眠 线程的调度执行中的细节
目录
线程核心回顾
等待一个线程-join()
休眠当前线程+线程状态
分析-线程的调度执行
线程核心回顾
Thread对象和内核中的线程是一一对应
可能出现内核中的线程已经结束销毁了,但是Thread对象还在:
Thread对象是受JVM管理的 而线程是操作系统内核控制的我们通过thread对象去让操作系统内核创建对应的线程 并执行 当线程中的run方法执行完毕 线程就销毁了 但是Thread对象可能还在没有被JVM回收
线程的终止:核心:让线程的入口方法,能够尽快结束.
islnterruptted()
interrupt()
每次执行循环,绝大部分时间都在sleep;主线程中调用Interrupt
t.interrupt(); 极大的概率下,此时t线程都是正在sleep中
这个操作,能够唤醒sleep的 InterruptedException sleep会抛出异常
针对异常的处理,使用break结束循环
针对上述代码,其实是sleep在搞鬼.
正常来说,调用lnterrupt方法就会修改islnterruptted方法内部的标志位. 设为true
由于上述代码中,是把sleep给唤醒了,
这种提前唤醒的情况下,sleep就会在唤醒之后,把 islnterruptted标志位给设置回false因此在这样的情况下,如果继续执行到循环的条件判定,就会发现能够继续执行
我个人的猜测是
sleep这样设定之后,相当于让程序员在catch语句中有更多的选择空间.
程序员可以自行决定,这个线程是要立即结束,还是等会再结束,还是不结束(忽略这个终止的信号)Java中的线程终止,不是一个"强制性"的措施
不是main 让t终止,t就一定终止.选择权在t自己手上(看t线程自己的代码咋写)C++这里的设定,太简单粗暴了. 由于线程是并发执行.
主线程中调用终止操作,t线程里面的工作完成到啥程度,不知道
就可能使得线程执行一半被终止,就可能得到一个"不上不下"的中间结果Java这里的设定,把决定权交给被终止的线程自己了.
线程内部代码中就可以保证,一定是执行出一些"阶段性"的结果,然后才真正退出
等待一个线程-join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作
例如,转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
比如,在主线程中调用t.join 就是让主线程等待t线程先结束.!!!
虽然可以通过sleep休眠的时间,来控制线程结束的顺序
但是,有的情况下,这样的设定并不科学
有的时候,就是希望t先结束,main就可以紧跟着结束了,此时通过设置时间的方式,不一定靠谱当执行到t.join,此时main线程就会"阻塞等待"
一直等到,t线程执行完毕,join才能继续执行看下控制台
这个就是在等待t.join
只要t线程不结束,主线程的join 就会一直一直的等待下去
join提供了带参数的版本,指定"超时时间"---等待的最大时间
如果3000之内,比如刚过了1000,t就结束了,此时join立即继续执行(不会等满3000)
如果超过3000,t还没结束,此时join也继续往下走,就不等了带有超时时间的等待,才是更科学的做法
计算机中,尤其是,和网络通信相关的逻辑,一般都是需要"超时时间"
计算机中(尤其是应用程序中)很难进行ns级别的精确时间的测量(误差比较大)
尤其是,线程本身调度开销往往就会达到ms级针对精确的时间计算,也不是完全做不到.
有一类操作系统,"实时操作系统"可以做到更精确的时间计算的(进行任务调度的时候,都会非常快,在一定时间误差之内完成一些计时相关操作)
咱们日常开发接触到的系统,
Windows,Linux,Mac,Android,都不是实时操作系统
(类似于this)哪个线程调用这个方法,返回哪个线程的引用.
休眠当前线程+线程状态
也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
代码调用sleep,相当于让当前线程,让出cpu的资源.
后续时间到了的时候,需要操作系统内核,把这个线程重新调到到cpu上,才能继续执行时间到,意味着,允许被调度了 而不是就立即执行了
sleep(0)使用sleep的特殊写法.
写了sleep(0)意味着让当前的线程,立即放弃cpu资源,等待操作系统重新调度
把cpu让出来给别人更多的执行机会Thread
1.创建线程2.关键属性 3.终止线程 4.线程等待 5.获取线程引用 6.线程休眠
举个例子:
刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是NEW状态;
当李四、王五开始去窗口排队,等待服务,就进入到RUNNABLE状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入
BLOCKED、WATING、TIMED_WAITING状态
如果李四、王五已经忙完,为TERMINATED状态。
所以,之前我们学过的isAlive()方法,可以认为是处于不是NEW和TERMINATED的状态都是活着的。NEW:安排了工作,还未开始行动new了Thread对象,还没start
TERMHINATED:工作完成了.内核中的线程已经结束了.但是Thread对象还在.
RUNNABLE:可工作的.又可以分成正在工作中和即将开始工作.
就绪
1)线程正在cpu上执行
2)线程随时可以去cpu上执行TIMED_WAITING:这几个都表示排队等着其他事情
指定时间的阻塞. 线程阻塞(不参与cpu调度,不继续执行了)
阻塞的时间是有上限的.
下面这个肯定是在等待的时间
sleep就是在阻塞
另外,join(时间)也是会进入到TIMED_WAITING状态:
1.jconsole/其他工具,查看当前的进程中的所有线程,找到你对应逻辑的线程是谁
2.看线程的状态是啥.
看到TIMED_WAITING/WAITING,怀疑是不是代码中某个方法产生阻塞,没有被及时唤醒
看到BLOCKED,怀疑是不是代码中出现死锁
看到RUNNABLE,线程本身没问题,考虑逻辑上某些条件没有预期触发之类的
3.再看线程具体的调用栈(尤其是阻塞的状态,线程代码阻塞在哪一行了....)
这俩线程,谁先join,谁后join无所谓
1)t1先结束,t2后结束 main 先在t1.join 阻塞等待 t1结束 main 再在t2.join 阻塞等待
t2结束 最终结果,打印的值,就是t1和t2都执行完的值
main继续执行后续打印
2)t2先结束,t1后结束 main 先在t1join 阻塞 t2结束,t1.join继续阻塞 t1结束,t1.join继续执行 由于t2已经结束了,此处的t2.join是不会阻塞的!!!! main 执行到t2.join
main继续执行后续打印
最终结果,打印的值,还是t1和t2都执行完的值
总的阻塞时间都是一样的 t1和t2较长的时间.
区别在于是分两个join各自阻塞一会 还是在一个join全都阻塞完
执行结果如上代码
这样的代码,很明显,就是有bug的!!!
实际执行效果和预期效果不符合,就叫bug
这样的问题,多线程并发执行引起的问题
如果把两个线程,变成,串行执行(一个完了,再执行另一个)
此时:
很明显,当前bug是由于多线程的并发执行代码引起的bug
这样的bug,就称为"线程安全问题"
或者叫做"线程不安全结论:
BLOCKED表示等待获取锁,WAITING和TIMED_WAITING表示等待其他线程发来通知.
TIMED_WAITING线程在等待唤醒,但设置了时限;WAITING线程在无限等待唤醒
分析-线程的调度执行
这个操作看起来是一行代码,实际上对应到3个cpu指令.
1.load,把内存中的值(count变量)读取到cpu寄存器
2.add,把指定寄存器中的值,进行+1操作(结果还是在这个寄存器中)
3.save,把寄存器中的值,写回到内存中.
CPU执行这三条指令的过程中,随时可能触发线程的调度切换的.123线程切走
12线程切走.....线程切回来3
1线程切走....线程切回来23
1线程切走....线程切回来2线程切走...线程切回来3
手机
由于操作系统,调度是"随机"的
执行任何一个指令的过程中,都可能触发上述的"线程切换"操作.这个过程是随机的不可预期的
随机调度,抢占式执行=>线程安全问题的罪魁祸首
线程安全[最核心的部分]
一段代码,如果在多线程并发执行的情况下,出现bug,就称为"线程不安全"
反之,没有bug,就是"线程安全"模拟线程的调度执行:(先执行的画上面,后执行的画下面)
两个线程在cpu上执行的时候 可能是并发 (在同一个cpu上执行)
也可能是并行(在不同的cpu上执行)两个线程,有不同的上下文(一组自己的寄存器的值) (存档)
分析一下上面的调度执行:
此时count的值就是2 因为t2的load的值就是t1 save的值为1 后面 t2再次add的时候值就是2
对于这个
当t1被调度回来之后
此时还是按照刚才执行的进度,继续往下执行明明是两次++ 最后的结果,还是1 (相当于一次++加丢了)
此时的值还是1:
实际在循环5w次过程中 也不知道有多少次,是正确的 也不知道有多少次,是错误的
最终执行的结果,一定是<=10w 可能会出现<5w的情况:概率低
通过上述讨论感受到,如果两个线程load到的数据都是0 意味着一定会少加一次.
如果一个load到0一个load到1,结果才是正确的一个线程的load得在另一个线程的save之后--才是正确的
其实也会存在另一种情况:其实是可能会出现<5w的情况(概率更低)
虽然是save了三次,但是最终的结果是1
其实如果数量较少,这种线程不安全的系数就会大大降低:
问题仍然存在,概率变低了
50次vs5w次 线程执行的时间长短是不同的
如果是循环50次,很可能在执行t2.start之前,t1就算完了
等后续t2再执行,变成纯串行的了