zoukankan      html  css  js  c++  java
  • Java 多线程 之 线程通信

    一、线程间通信

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

      例如:使用两个线程打印 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 }

     

     

  • 相关阅读:
    求幂运算、多项式乘法及Horner法则的应用
    JAVA泛型中的类型擦除及为什么不支持泛型数组
    关于递归的理解及递归表达式复杂度分析(以求解最大公约数为例)
    随机序列生成算法---生成前N个整数的一组随机序列
    Windows 与 Linux下关于端口不能访问的问题
    Netty 实现HTTP文件服务器
    字符数组转换成数字
    字符串反转的进一步应用----单词反转
    递归算法编程整数因子分解问题的递归算法
    数据返回[数据库基础]——图解JOIN
  • 原文地址:https://www.cnblogs.com/niujifei/p/14405581.html
Copyright © 2011-2022 走看看