全面理解线程间的通信方式

因为存在对共享变量的操作,才有了讨论线程的话题。在线程中存在这样一种场景,一个线程操作了共享变量的值而另一个线程感知了此次操作,然后进行相应的操作。整个过程开始于一个线程,结束与另一个线程。前者我们可以称作生产者,后者我们可以称作消费者,因为有了前者的活动才出发后者逻辑的发生,这种隔离模式在功能实现方面具备了良好的伸缩性。等待/通知的java方法是任何对象都具备的,因为这些方法被定义在java的超类java.lang.Object上下图展示了Object 类的所有方法:

等待、通知机制就是,线程A调用了对象Owait()方法进入等待状态,线程B 调用了对象Onotify()或者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()所要注意的点:

  1. 调用 wait()/notify()/notifyAll() 方法的线程必须是持有该对象的锁的线程
  2. 调用wait()方法后线程由RUNNING状态变为WAITING状态,并将当前线程放置在该对象的等待队列中,同时释放拥有的锁
  3. 调用notify()notifyAll()之后线程并不会立即从wait()方法处返回,而是需要等待调用notify()/notifyAll()的线程释放锁之后才会返回。
  4. 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()都是锁级别的操作,操作这几个方法的线程必须持有该对象的锁,而锁又是属于对象的。每一个对象的对象头中有几位是标识锁的状态的,所以实际上锁是属于对象的并不是属于线程的。如果这几个方法定义在线程中会造成极大的不便,在实际的操作中我们会遇到一个线程获取几把锁的情况,如果将锁定义在线程中时间这种情况就不是那么的方便了。

3wait()方法是属于对象的,那调用Thread.wait()会怎样?

调用Thread.wait() 也就是说将Thread 当做锁对象,持有Thread对象的锁的线程在执行结束后会自动调用notify(),所以我们应该避免使用线程对象来作为锁对象。

4notifyAll() 会唤醒所有的线程同时去争夺这把锁,如果没有获取到锁的对象该怎么办?

没有抢到锁的线程会再次进入WAITING状态,进入对象的等待队列中去,直至有其他线程再次调用notify()或者notify()All 或者调用该线程的中断方法。