全面理解线程间的通信方式
- 2020 年 5 月 8 日
- 筆記
- JAVA, notify() notifyAll, wait, 多线程
因为存在对共享变量的操作,才有了讨论线程的话题。在线程中存在这样一种场景,一个线程操作了共享变量的值而另一个线程感知了此次操作,然后进行相应的操作。整个过程开始于一个线程,结束与另一个线程。前者我们可以称作生产者,后者我们可以称作消费者,因为有了前者的活动才出发后者逻辑的发生,这种隔离模式在功能实现方面具备了良好的伸缩性。等待/通知的java方法是任何对象都具备的,因为这些方法被定义在java的超类java.lang.Object上下图展示了Object 类的所有方法:
等待、通知机制就是,线程A调用了对象O的wait()方法进入等待状态,线程B 调用了对象O的notify()或者notifyAll() 唤醒处于对象O上等待的线程A,使线程A 从wait()方法处返回从而进行剩余操作。上述两个线程通过对象O来进行交互,而对象O上的wait()/notify()/notifyAll()就如同信号一般控制着线程的操作。这种例子生活中随处可见,例如商品入库,如果仓库中是满的我们就无法将商品再放入仓库,如果仓库中没有商品我们也无法从仓库中取出商品,再举一个日常发生在我们身边的场景。现在网购越来越方便,我们与快递之间有一个快递小哥在关联,快递小哥将包裹放到快递柜,我们去快递柜领取快递:
1 /** 2 * 快递 3 */ 4 public class Courier extends Thread { 5 6 @SneakyThrows 7 @Override 8 public void run() { 9 10 synchronized (CourierCabinet.CABINET) { 11 while (true) { 12 if (CourierCabinet.CABINET.size() == 10) { 13 //歇一歇吧 快递柜没地方了 14 CourierCabinet.CABINET.wait(); 15 } 16 CourierCabinet.CABINET.add("包裹"); 17 System.out.println("亲爱的顾客您的快递已入柜,请及时来领取"); 18 CourierCabinet.CABINET.notify(); 19 Thread.sleep(100); 20 21 } 22 } 23 24 } 25 }
1 /** 2 * 收件人 3 */ 4 public class Recipient extends Thread { 5 6 7 @SneakyThrows 8 @Override 9 public void run() { 10 11 while (true) { 12 synchronized (CourierCabinet.CABINET) { 13 14 if (CourierCabinet.CABINET.size() == 0) { 15 // 快递员还未将包裹入柜 没法领取等一等 16 CourierCabinet.CABINET.wait(); 17 } 18 19 CourierCabinet.CABINET.remove("包裹"); 20 System.out.println("哈哈 领到了我的快递..."); 21 CourierCabinet.CABINET.notify(); 22 Thread.sleep(100); 23 } 24 } 25 } 26 }
1 ** 2 * 快递柜 3 */ 4 public class CourierCabinet { 5 6 /** 7 * 快递柜容量 8 */ 9 public static final List<String> CABINET = new ArrayList<>(10); 10 11 12 public static void main(String[] args) { 13 14 15 new Thread(new Courier()).start(); 16 17 18 new Thread(new Recipient()).start(); 19 20 21 } 22 23 }
运行结果:
通过以上例子,可以总结出 使用对象的wait()/notify()/notifyAll()所要注意的点:
- 调用 wait()/notify()/notifyAll() 方法的线程必须是持有该对象的锁的线程
- 调用wait()方法后线程由RUNNING状态变为WAITING状态,并将当前线程放置在该对象的等待队列中,同时释放拥有的锁
- 调用notify()和notifyAll()之后线程并不会立即从wait()方法处返回,而是需要等待调用notify()/notifyAll()的线程释放锁之后才会返回。
- notify() 将等待队列中的线程移动到同步队列中去,notifyAll()将等待队列中所有的线程移动到同步队列中去,此时被移动的线程状态由WAITING 转为BLOCKED
关于线程同步、通知机制面试题
1:为什么操作 wait() notify() notifyAll() 需要事先获取锁,
主要是为了防止死锁了永久等待的发生,以上面的例子说明,收件人线程执行if(CABINET.size()==0)的时候满足条件 由于没有synchronize 加持,所以该线程并不一定会执行CABINET.wait() 可能被CPU切走了,线程进入了BLOCKED状态。
此时快递小哥线程获取到了执行权,判断if(CABINET.size==10)不满足条件,然后执行CABINET.add(包裹操作)执行notify()因为收件人线程并没有执行wait(),所以就可能处于一直等待中。就如同你给我打电话 我还没有拿到电话你就已经打过了 此时我再拿到电话也不会收到你的电话了。
Wait()放到synchronize 中执行就是为了保证线程安全,如果一个线程想要从wait()处返回也需要获取到该对象的锁否则会出现IllegalMonitorStateException异常。
2:为什么线程通信的方法wait()notify()notifyAll()是定义在Object中而sleep()定义在线程类中?
主要因为java中的wait()notify()notifyAll()都是锁级别的操作,操作这几个方法的线程必须持有该对象的锁,而锁又是属于对象的。每一个对象的对象头中有几位是标识锁的状态的,所以实际上锁是属于对象的并不是属于线程的。如果这几个方法定义在线程中会造成极大的不便,在实际的操作中我们会遇到一个线程获取几把锁的情况,如果将锁定义在线程中时间这种情况就不是那么的方便了。
3:wait()方法是属于对象的,那调用Thread.wait()会怎样?
调用Thread.wait() 也就是说将Thread 当做锁对象,持有Thread对象的锁的线程在执行结束后会自动调用notify(),所以我们应该避免使用线程对象来作为锁对象。
4:notifyAll() 会唤醒所有的线程同时去争夺这把锁,如果没有获取到锁的对象该怎么办?
没有抢到锁的线程会再次进入WAITING状态,进入对象的等待队列中去,直至有其他线程再次调用notify()或者notify()All 或者调用该线程的中断方法。