多个线程间肯定是需要通信的,回忆一下我们的并发编程模型里的流水线工作模式(流水线工作模式),我们的线程在遇到io阻塞的时候,肯定不能在那边一直等着,得出让自己的cpu资源,等到io完成了,再把任务交给下一个工作线程,我们试着来实现下看看。
第一种方式,共享变量的方式。
package top.huster.thread.notify; /** * Created by longan.rtb on 2018/10/31. */ public class WaitOnlineImpl { private static boolean finishedStatus = false; public static void main(String[] args) { Thread thread1 = new Thread(new Runnable() { @Override public void run() { System.out.print("我的工作做完了..."); synchronized(this) { finishedStatus = true; } } }); Thread thread2 = new Thread(() -> { while (!finishedStatus) { //not finish continue waiting //System.out.print("...."); } System.out.print("我可以开始我的工作了..."); }); thread2.start(); try { Thread.sleep(1000L); } catch (Exception e) { System.err.print("error"); } thread1.start(); } } //打印输入结果如下 我的工作做完了...我可以开始我的工作了...
问题,这种方式有个很明显的问题,线程2一直在while循环等待状态的改变,所以cpu在空转。
通知机制。幸好,java给我们提供了其他的方法。wait、notify·、notifyAll这三个方法能够让我们做到很多事情。后面的锁的实现很多也依赖于这个,所以一定要搞清楚他们。
使用wait、notify的方式实现如下
package top.huster.thread.notify; /** * Created by longan.rtb on 2018/10/31. */ public class WaitNotify1Impl { private final static Object bell = new Object(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 我的工作做完了..."); synchronized (bell) { bell.notify(); } }); Thread thread2 = new Thread( () -> { synchronized (bell) { try { System.out.println(Thread.currentThread().getName() + " I am waiting for somethings...."); bell.wait(); } catch (Exception e) { // do something } } System.out.println(Thread.currentThread().getName() + " 我开始我的工作....."); }); thread2.start(); try { Thread.sleep(1); } catch (Exception e) { //do something } thread1.start(); } } /*** 输出结果 Thread-1 I am waiting for somethings.... Thread-0 我的工作做完了... Thread-1 我开始我的工作..... **/
成功的解决了空转的问题,但是我们仔细想一想会不会有问题呢?如果我们的程序先启动过了线程1,后启动了线程2,会有什么问题呢?
我们交换一下代码顺序,修改这几行代码如下
thread1.start(); try { Thread.sleep(1); } catch (Exception e) { //do something } thread2.start();
运行结果如下,你会发现线程会一直等待而无法结束。
Thread-0 我的工作做完了... Thread-1 I am waiting for somethings....
这个叫做信号丢失。也许简单的程序你不会犯错,但是程序一旦复杂,这种犯错的概率大大增加。那么如何来解决这个问题呢? 其实也很容易,我们设置一个标志位,如果已经notify了,则把这个标志位置为true,那么等待的线程如果发现标志位已经为true了,就不用wait了,修改代码如下:
package top.huster.thread.notify; /** * Created by longan.rtb on 2018/10/31. */ public class WaitNotify1Impl { private final static Object bell = new Object(); private static boolean hasNotified = false; public static void main(String[] args) { Thread thread1 = new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 我的工作做完了..."); synchronized (bell) { bell.notify(); hasNotified = true; } }); Thread thread2 = new Thread( () -> { synchronized (bell) { if (!hasNotified) { try { System.out.println(Thread.currentThread().getName() + " I am waiting for somethings...."); bell.wait(); } catch (Exception e) { // do something } } } System.out.println(Thread.currentThread().getName() + " 我开始我的工作....."); }); thread1.start(); try { Thread.sleep(1); } catch (Exception e) { //do something } thread2.start(); } } /*** 输出结果如下 ,两个线程又愉快的生活在了一起。 Thread-0 我的工作做完了... Thread-1 我开始我的工作..... **/
是否现在就万事无忧了呢? 也不是,因为还有个问题叫,假唤醒。wait的线程在没有接收到notify的情况下,无缘无故的醒来,执行自己的逻辑。这个时候,因为依赖的notify线程并没有完成自己的工作,所以逻辑就出错了。这是我们不愿意看到的。
这个时候只需要把if的条件改成while就可以了,修改如下
Thread thread2 = new Thread( () -> { synchronized (bell) { while (!hasNotified) { try { System.out.println(Thread.currentThread().getName() + " I am waiting for somethings...."); bell.wait(); } catch (Exception e) { // do something } } } System.out.println(Thread.currentThread().getName() + " 我开始我的工作....."); });
最开始我怎么也无法理解这个while替换if的妙处,因为我看到的文章并没有写任何逻辑,也就是线程没有做任何工作,我把print这个当做工作,写在while里,我想如果线程无端醒过来,不就继续执行他的工作了,而跟if和while没关系吗? 知道我把线程的实际工作从while拿出来才恍然大悟。只要标志位没有被设置为true,那么一直会在while循环里。都会执行wait逻辑,这个时候如果线程无端醒来,因为hasNotified还是false,因为又执行了一遍while逻辑,也就是再次进入wait。一直到标志位的值发生了改变,线程才有机会执行自己的逻辑。
实际的应用。我们使用wait和notify来实现一个锁试试。
package top.huster.thread.notify; /** * Created by longan.rtb on 2018/11/1. */ public class LockTest { private int count = 0; private Lock lock = new Lock(); public void increase() { lock.lock(); count++; lock.unlock(); } class Lock { private boolean hasLocked = false; void lock() { synchronized(this) { while (hasLocked) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } hasLocked = true; } } void unlock() { hasLocked = false; notify(); } } }
分析: 要特别注意while那一段代码,这个就是我们刚刚上面讲述的内容,如果没有这个while会导致这个lock错失notify的信号的话,就永远wait下去。而如果不用while用if的话,如果遇到假唤醒就会导致同时有两个线程进入了lock锁定区域,这个也与我们想要的不符合。
《理解Java线程间的通信》有1个想法