一、线程间通信

  概念:多个线程在处理同一资源,但是处理的动作(线程的任务)却不相同。

  例如:使用两个线程打印 1-100。线程1, 线程2 交替打印,怎么实现呢?

  为什么要处理线程间通信:

    多个线程并发执行,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成同一件任务,并且希望它们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

  如何保证线程间通信有效利用资源:

    多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。

    即多个线程在操作同一份数据时,避免对统一共享变量的争夺。

    这时需要通过一定的手段使各个线程能有效的利用资源,这种手段称为——等待唤醒机制。

  

二、案例一:使用两个线程打印 1-100。线程1, 线程交替打印

  示例:

 1 public class CommunicationTest {
 2 
 3     public static void main(String[] args) {
 4         Number number = new Number();
 5 
 6         Thread t1 = new Thread(number, "线程1");
 7         Thread t2 = new Thread(number, "线程2");
 8 
 9         t1.start();
10         t2.start();
11     }
12 }
13 
14 class Number implements Runnable {
15 
16     private int number = 1;
17     private Object obj = new Object();
18 
19     @Override
20     public void run() {
21         while (true) {
22             synchronized (this) {
23 
24                 notify();
25 
26                 if (number <= 100) {
27 
28                     try {
29                         Thread.sleep(10);
30                     } catch (InterruptedException e) {
31                         e.printStackTrace();
32                     }
33 
34                     System.out.println(Thread.currentThread().getName() + ":-:" + number);
35                     number++;
36 
37                     try {
38                         wait();  //使得调用如下wait()方法的线程进入阻塞状态
39                     } catch (InterruptedException e) {
40                         e.printStackTrace();
41                     }
42                 }
43             }
44         }
45     }
46 }

 

  运行结果可以看到线程1和线程2是相互交替打印出来的,此时这里的同步监视器是 this,即此类的对象。

  如果我们把锁对象换成了上面 声明的 Object 对象,这时也要用 obj 对象来调用 wait() 与 notify() 方法。否则将会出现下面的异常:

 

  示例:

 1 public class CommunicationTest {
 2 
 3     public static void main(String[] args) {
 4         Number number = new Number();
 5 
 6         Thread t1 = new Thread(number, "线程1");
 7         Thread t2 = new Thread(number, "线程2");
 8 
 9         t1.start();
10         t2.start();
11     }
12 }
13 
14 class Number implements Runnable {
15 
16     private int number = 1;
17     private Object obj = new Object();
18 
19     @Override
20     public void run() {
21         while (true) {
22             synchronized (obj) {
23 
24                 obj.notify();
25 
26                 if (number <= 100) {
27 
28                     try {
29                         Thread.sleep(10);
30                     } catch (InterruptedException e) {
31                         e.printStackTrace();
32                     }
33 
34                     System.out.println(Thread.currentThread().getName() + ":-:" + number);
35                     number++;
36 
37                     try {
38                         obj.wait();
39                     } catch (InterruptedException e) {
40                         e.printStackTrace();
41                     }
42                 }
43             }
44         }
45     }
46 }

 

三、等待唤醒机制

  1、什么是等待唤醒机制

    这是多个线程间的一种协作机制。线程之间常见的就是线程间的竞争(race),但是线程之间也会有协作机制。好比同一公司员工,存在晋升的时候,也有合作的时候。

    当一个线程进行了规定操作后,就进入等待状态(wait)等待其他线程执行完他们的指定代码后,再将其唤醒(notify);在有多个线程进行等待时,如果需要,使用 notifyAll() 来唤醒所有的等待线程。

    wait / notify 就是线程间的一种协作机制。

  2、等待唤醒机制中常用方法

    等待唤醒机制就是用于解决线程间通信的问题,常用方法如下:

    (1)wait

      ① 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。

      ② 线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 waiting。该线程要等着别的线程执行一个特别的动作,即 “通知(notify)” 在这个对象上等待的线程从 wait set 中释放出来,重写进入到调度队列(ready queue)中。

      ③ 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

      ④ 调用此方法后,当前线程将释放对象监控权 ,然后进入等待;

      ⑤ 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

    (2)notify

      一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。

      则选取所通知对象的 wait set 中的一个线程释放;例如:餐馆有空位置后,等待就餐最久的顾客先入座。

      调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

    (3)notifyAll

      一旦执行此方法,就会唤醒所有被wait的线程。

      调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

      注意:

      哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不支持有锁,所有它需要再次尝试去获取锁(很可能面临其他线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

     小节:

       如果能获取锁,线程就从 waiting 状态变成 Runnable 状态;

      ② 没有获取锁,从 wait set 出来,又进入 entry set,线程就从 waiting 状态又变成 blocked 状态。

  3、等待唤醒机制需要注意的细节

    (1)wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器

        原因:对应的锁对象可以通过 notify 唤醒使用同一个锁对象调用的 wait 方法后的线程,否则,会出现IllegalMonitorStateException异常。

    (2)wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中

        原因:锁对象可以是任意对象,而任意对象的所属类都是继承 Object 类的。

    (3)wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中

        原因:必须要通过锁对象调用这两个方法。

  4、面试题:sleep() 和 wait() 的异同?

    相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

    不同点:① 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait();

        ② 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中;

        ③ 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁;

        ④ wait() 必须有“锁,监视器”对象来调用,如果由别的对象调用会报IllegleMoniterStateException;

          sleep()是静态方法,Thread类名调用就可以;

        ⑤ wait()使得当前线程进入阻塞状态后,由notify唤醒;sleep()使得当前线程进入阻塞状态后,时间到或被interrupt()醒来。

 

四、案例二

  1、生产者/消费者问题:

    生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

  2、这里可能出现两个问题

    ① 生产者比消费者快时,消费者会漏掉一些数据没有取到;

    ② 消费者比生产者快时,消费者会取到相同的数据。

  3、代码实现:

  1 public class ProductTest {
  2     public static void main(String[] args) {
  3         Clerk clerk = new Clerk();
  4 
  5         Producer p1 = new Producer(clerk);
  6         p1.setName("生产者1");
  7 
  8         Consumer c1 = new Consumer(clerk);
  9         c1.setName("消费者1");
 10         Consumer c2 = new Consumer(clerk);
 11         c2.setName("消费者2");
 12 
 13         p1.start();
 14         c1.start();
 15         c2.start();
 16 
 17     }
 18 }
 19 
 20 //店员
 21 class Clerk {
 22     private int productCount = 0;
 23 
 24 
 25     /**
 26      * 生产产品
 27      */
 28     public synchronized void produceProduct() {
 29         if (productCount < 20) {
 30             productCount++;
 31             System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
 32 
 33             notify();
 34 
 35         } else {
 36             try {
 37                 wait();
 38             } catch (InterruptedException e) {
 39                 e.printStackTrace();
 40             }
 41         }
 42     }
 43 
 44     /**
 45      * 消费产品
 46      */
 47     public synchronized void consumeProduct() {
 48 
 49         if (productCount > 0) {
 50             System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
 51             productCount--;
 52 
 53             notify();
 54 
 55         } else {
 56             try {
 57                 wait();
 58             } catch (InterruptedException e) {
 59                 e.printStackTrace();
 60             }
 61         }
 62     }
 63 }
 64 
 65 
 66 //生产者
 67 class Producer extends Thread {
 68 
 69     private Clerk clerk;
 70 
 71     public Producer(Clerk clerk) {
 72         this.clerk = clerk;
 73     }
 74 
 75     @Override
 76     public void run() {
 77         System.out.println(getName() + ":开始生产产品.....");
 78 
 79         while (true) {
 80             try {
 81                 Thread.sleep(10);
 82             } catch (InterruptedException e) {
 83                 e.printStackTrace();
 84             }
 85             clerk.produceProduct();
 86         }
 87     }
 88 }
 89 
 90 //消费者
 91 class Consumer extends Thread {
 92     private Clerk clerk;
 93 
 94     public Consumer(Clerk clerk) {
 95         this.clerk = clerk;
 96     }
 97 
 98     @Override
 99     public void run() {
100         System.out.println(getName() + ":开始消费产品.....");
101 
102         while (true) {
103             try {
104                 Thread.sleep(20);
105             } catch (InterruptedException e) {
106                 e.printStackTrace();
107             }
108             clerk.consumeProduct();
109         }
110     }
111 }

 

五、案例三

  1、拿生产包子消费包子来描述等待唤醒机制如何有效利用资源

    包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。
接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

    线程 A 用来生成包子的,线程 B 用来吃包子的,包子可以理解为同一资源,线程 A 与线程 B 处理的动作,一个是生产,一个是消费,那么线程 A 与线程 B 之间就存在线程通信问题。

                   

  2、代码示例

  1 public class BaoziTest {
  2     public static void main(String[] args) {
  3         //创建包子对象;
  4         Baozi bz =new Baozi();
  5         //创建包子铺线程,开启,生产包子;
  6         new BaoziPu(bz).start();
  7         //创建吃货线程,开启,吃包子;
  8         new ChiHuo(bz).start();
  9     }
 10 }
 11 
 12 class Baozi {
 13     String pi;
 14     String xian;
 15     //包子的状态:有 true,没有 false,设置初始值 false没有包子
 16     boolean flag = false;
 17 }
 18 
 19 class BaoziPu extends Thread {
 20     //1. 需要在成员位置创建一个包子变量
 21     private Baozi bz;
 22 
 23     //2. 使用参数构造方法,为这个包子变量赋值
 24     public BaoziPu(Baozi bz) {
 25         this.bz = bz;
 26     }
 27 
 28     //设置线程任务(run):生产包子
 29     @Override
 30     public void run() {
 31         //定义一个变量
 32         int count = 0;
 33         //让包子铺一直生产包子
 34         while (true) {
 35             //必须同时同步技术保证两个线程只能有一个正执行
 36             synchronized (bz) {
 37                 //对包子的状态进行判断
 38                 if (bz.flag == true) {
 39                     //包子铺调用 wait 方法进入等待状态
 40                     try {
 41                         bz.wait();
 42                     } catch (InterruptedException e) {
 43                         e.printStackTrace();
 44                     }
 45                 }
 46 
 47                 //被唤醒之后执行,包子铺生产包子
 48                 //增加一些趣味性:交替生产两种包子
 49                 if (count % 2 == 0) {
 50                     //生产 薄皮三鲜馅包子
 51                     bz.pi = "薄皮";
 52                     bz.xian = "三鲜馅";
 53                 } else {
 54                     //生产 冰皮 牛肉大葱陷
 55                     bz.pi = "冰皮";
 56                     bz.xian = "牛肉大葱陷";
 57                 }
 58 
 59                 count++;
 60                 System.out.println("包子铺正在生产:" + bz.pi + bz.xian + "包子");
 61                 //生产包子需要3秒钟
 62                 try {
 63                     Thread.sleep(300);
 64                 } catch (InterruptedException e) {
 65                     e.printStackTrace();
 66                 }
 67 
 68                 //包子铺生产好了包子
 69                 //修改包子的状态为true有
 70                 bz.flag = true;
 71                 //唤醒消费者线程,让顾客线程吃包子
 72                 bz.notify();
 73                 System.out.println("包子铺已经生产好了:"+bz.pi+bz.xian+"包子,吃货可以开始吃了");
 74             }
 75         }
 76     }
 77 }
 78 
 79 /**
 80  * 消费者
 81  */
 82 class ChiHuo extends Thread {
 83     //1.需要在成员位置创建一个包子变量
 84     private Baozi bz;
 85 
 86     //2.使用带参数构造方法,为这个包子变量赋值
 87     public ChiHuo(Baozi bz) {
 88         this.bz = bz;
 89     }
 90 
 91     //设置线程任务(run):吃包子
 92     @Override
 93     public void run() {
 94         //使用死循环,让吃货一直吃包子
 95         while (true) {
 96             //必须同时同步技术保证两个线程只能有一个在执行
 97             synchronized (bz){
 98                 //对包子的状态进行判断
 99                 if(bz.flag==false){
100                     //吃货调用wait方法进入等待状态
101                     try {
102                         bz.wait();
103                     } catch (InterruptedException e) {
104                         e.printStackTrace();
105                     }
106                 }
107 
108                 //被唤醒之后执行的代码,吃包子
109                 System.out.println("吃货正在吃:"+bz.pi+bz.xian+"的包子");
110                 //吃货吃完包子
111                 //修改包子的状态为false没有
112                 bz.flag = false;
113                 //吃货唤醒包子铺线程,生产包子
114                 bz.notify();
115                 System.out.println("吃货已经把:"+bz.pi+bz.xian+"的包子吃完了,包子铺开始生产包子");
116                 System.out.println("----------------------------------------------------");
117             }
118         }
119     }
120 }

 

 

 

 

posted on 2021-02-15 22:09  格物致知_Tony  阅读(686)  评论(0编辑  收藏  举报