Java Multi-Threads Running in Sequence
最近看到一篇Java多线程顺序打印ABC的文章,用到了wait(), notify(), 感觉写的比较粗糙,这里写一写自己的看法,对他的第一段代码,在变量上稍微做了一些调整a->locka,b->lockb,c->lockc,其他的基本没改动。
预备知识
- wait是释放锁,不用等synchronized语句块执行完毕,阻塞,之后等待被重新notify
- notify是释放锁,但释放锁是在synchronized语句块执行完毕后,不阻塞,继续执行下去。
- yield不释放锁,阻塞,但释放cpu资源
- sleep不释放锁,阻塞,同时占用cpu资源
上代码
1 | package com.iovi.concurrence; |
有死锁?
程序刚开始运行时,到T3时,pa,pb,pc都执行了,则死锁。在T2位置加一行Thead.sleep(1000)
,基本就死锁了。
另外,在程序结束时也会产生死锁,下面是最后一轮打印:
pa打印A,countpa=0时,pa卡在lockc.wait(),等待pc执行lockc.notify();
pc打印C,countpc=0,lockc.notify(),pa被唤醒,pc卡在lockb.wait();
pb打印B,countpb=0,lockb.notify(),pc被唤醒,卡在locka.wait();
因此,结果可想而知,pb永远处于死锁状态,而pa,pc谁先跳出循环,应该是随机的,不一定是pa先。因此,更加合理的做法是T13改成prev.wait(1000)
。
打印顺序有什么规律?
我们先讨论下第一轮打印的顺序,通常是ACB、也有可能是ABC,当然,非A开头也不是没有可能,一般第一轮确定后,后面顺序基本确定,但也不是一定的,只是可能性非常大,后面会详细分析为什么顺序可能会变,这里这讨论顺序的规律。
我们这里获得锁
,指的是程序语句synchronized(lock)
执行完毕。不考虑死锁的情况下、使用多核CPU,第一轮顺序通常是ACB,这里分析下原因:
pa占有lockc,locka时,不影响pc占有lockb,pc要执行,先获得lockb,再获得lockc,很大概率上,pc已获得lockb,只需要lockc;而pb执行先要获得locka,再获得lockb,而locka需要等pa释放,且pb需要先于pc抢占lockb。基于上面的分析,很可能pc是第二个打印,这里有一个前提,CPU多核。
但并不是说,pa打印完后,pc一定占有lockb,也可能pa打印完后,pb获得locka,并且先于pc获得lockb,那么这个顺序就变成了pb第二个打印,CPU单核情况下,很可能发生。一旦这个顺序确定后,后面就基本不太可能变化。
顺序一旦确定,不再改变?
从每次运行结果来看,顺序似乎按照三个顺序下来,即头三个ACB,则后面都是重复这个顺序,先搞清楚下面的一些概念:
- pa执行lockc.wait()后,需要pc执行lockc.notify()才能继续执行,否则处于休眠状态
- pb执行locka.wait()后,需要pa执行locka.notify()才能继续执行,否则处于休眠状态
- pc执行lockb.wait()后,需要pb执行lockb.notify()才能继续执行,否则处于休眠状态
假设A第一个打印,C第二个打印,B第三个打印,那么第二趟开头的可能是A,也可能是C,但不可能是B:
A要先打印,则需要等pc执行lockc.notify(),这一条没问题,然后先后获得lockc、locka,这条也没问题,pb执行locka.wait()时已经满足;
C要先打印,则需要等pb执行lockb.notify(),这一条没问题,然后获得lockb,且先于pa获得lockc;
我们假设的第一轮打印顺序是ACB,那么lockc.notify()先于lockb.notify()执行,很大可能,pa是先于pc获得lockc,那么很大程度上,pa先于pc打印,即第二轮第一个很可能是A。
好了,再思考下第二轮第二个会是什么,其实跟上面的类似:
C要先打印,则需要等pb执行lockb.notify(),这一条没问题,然后先后获得lockb、lockc,这条没问题,pa执行lockc.wait()时已经满足;
B要先打印,则需要等pa执行locka.notify(),这一条没问题,然后获得locka,且先于pc获得lockb;
我们假设已经打印了ACBA,那么lockb.notify()先于locka.notify()执行,很大可能,pc是先于pb获得lockb,那么很大程度上,C要先于B打印,即第二轮第二个很可能是C.
至于第二轮第三个,按照相同的思路,可以得出是B,后面循环按照这个思路进行即可。
由此,可以推断出一旦第一轮顺序确定,后面也就确定了,但不是绝对的。
如何顺序打印
从上面的结论来看,要顺序打印,只要第一轮顺序即可,那么其实也很简单,只要做到第一轮第二个是B,就可以顺序执行,只要将线程启动语句改成:
1 | new Thread(pa).start(); |
这样做还能避免一开始死锁的情况。
如何有节奏地打印
T10位置添加:
1 | Thread.sleep(1000); |
创建线程使用以下语句:
1 | MyThreadPrinter pa = new MyThreadPrinter("A", lockb, locka); |
对原函数做了些许调整:
private MyThreadPrinter(String name, Object prev, Object self)
改为
private MyThreadPrinter(String name, Object next, Object self)
即prev–>next,其实也很好理解,A要先于B打印,A要先获得B的锁,那么B就肯定落后于A,对于C,B要先获得C的锁,那么顺序就确定下来了。这里我们要不要考虑CPU多核?CPU多核影响的是第一轮第二个打印字母,有没有可能第二个是C,而不是B?其实在T10添加休眠语句后,pb肯定先于pc获得lockc,那么第二个打印的字母肯定是B了。
不知道读者心中有没有疑问,为什么要在T10位置休眠,如果语句放在T11或者T12,其实效果是一样的,但这里有一个概念需要额外注意:
notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后
谈到线程阻塞,还可以参考Java Thread.join(), 《 JAVA wait(), notify(),sleep详解》