• 聊聊并发(二)——生产者与消费者


    一、等待唤醒机制

    1、介绍

      wait():一旦执行此方法,当前线程进入阻塞状态,并释放锁。
      notify():一旦执行此方法,就会唤醒一个被wait()的线程。如果有多个,就唤醒优先级高的,如果优先级一样,则随机唤醒一个。
      notifyAll():一旦执行此方法,会唤醒所有wait()的线程。
      notify()唤醒线程,不会立即释放锁对象,需要等到当前同步代码块都执行完后才会释放锁对象。下次和被唤醒的线程同时竞争锁对象。
      问:wait 等待中的线程被 notify 唤醒了会立马执行吗?
      答:不会。被唤醒的线程需要重新竞争锁对象,获得锁的线程可以从wait处继续往下执行。

    2、两个线程交替打印问题

      如何使用两个线程交替打印1—100?
      代码示例:先用两个线程来打印1—100。

     1 // 不写注释也能看懂的代码
     2 public class Main {
     3 
     4     public static void main(String[] args) {
     5         Num num = new Num();
     6         Thread thread1 = new Thread(num);
     7         Thread thread2 = new Thread(num);
     8 
     9         thread1.start();
    10         thread2.start();
    11     }
    12 }
    13 
    14 
    15 class Num implements Runnable {
    16 
    17     private int i = 1;
    18 
    19     @Override
    20     public void run() {
    21         while (true) {
    22             synchronized (this) {
    23                 if (i <= 100) {
    24                     System.out.println(Thread.currentThread().getName() + ":" + i);
    25                     i++;
    26                 } else {
    27                     break;
    28                 }
    29             }
    30         }
    31     }
    32 }
    33 
    34 // 可能的结果.当然是谁抢到谁打印.
    35 Thread-0:1
    36 Thread-0:2
    37 Thread-1:3
    38 Thread-1:4
    39 ……

      理解:两个线程的共享变量是 i ;两个线程共同竞争的锁 this 是num。

      再看原问题,线程本来是抢占式的,要想实现交替打印。显然,需要线程之间有通信。即,线程A打印 1 之后,阻塞一下,等待线程B打印 2 ,然后唤醒A,并且B阻塞,A打印3,以此内推。这就是线程的等待唤醒机制。
      代码示例:只需要在上述代码添加两行即可,如下:

     1 class Num implements Runnable {
     2 
     3     private int i = 1;
     4 
     5     @Override
     6     public void run() {
     7         while (true) {
     8             synchronized (this) {
     9                 // 1.先唤醒对方
    10                 notify();
    11 
    12                 if (i <= 100) {
    13                     System.out.println(Thread.currentThread().getName() + ":" + i);
    14                     i++;
    15 
    16                     // 2.当前线程操作完后.等待阻塞
    17                     try {
    18                         wait();
    19                     } catch (InterruptedException e) {
    20                         e.printStackTrace();
    21                     }
    22                 } else {
    23                     break;
    24                 }
    25             }
    26         }
    27     }
    28 }

      图解:

      代码示例:将上述代码改用Lock实现。

     1 class Num implements Runnable {
     2 
     3     private int i = 1;
     4     //
     5     final private Lock lock = new ReentrantLock();
     6     final Condition condition = lock.newCondition();
     7 
     8     @Override
     9     public void run() {
    10         while (true) {
    11             // 上锁
    12             lock.lock();
    13 
    14             try {
    15                 // 1.先唤醒对方
    16                 condition.signal();
    17 
    18                 if (i <= 100) {
    19                     System.out.println(Thread.currentThread().getName() + ":" + i);
    20                     i++;
    21 
    22                     // 2.当前线程操作完后.等待阻塞
    23                     try {
    24                         condition.await();
    25                     } catch (InterruptedException e) {
    26                         e.printStackTrace();
    27                     }
    28                 } else {
    29                     break;
    30                 }
    31             } finally {
    32                 // 释放锁
    33                 lock.unlock();
    34             }
    35         }
    36     }
    37 }

      使用lock同步锁,就不需要sychronized关键字了,需要创建lock对象和condition实例。Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。
    在 Condition 对象中,对应的等待唤醒方法需要改为:
      wait()方法——await()方法
      signal()方法——notify()方法
      signalAll()——notifyAll()方法

    3、三个线程交替打印问题

      在上个问题的基础上,升级一下,考虑三个线程交替打印1—99?
      思想同理:接力棒A,交给B,B交给C,C交给A。但是如何指定唤醒一个线程呢?notify()只能随机唤醒一个。这里用lock的condition来解决。
      代码示例:三个线程交替打印

      1 public class Main {
      2 
      3     public static void main(String[] args) {
      4         Num num = new Num();
      5 
      6         new Thread(() -> {
      7             num.loopA();
      8         }).start();
      9 
     10         new Thread(() -> {
     11             num.loopB();
     12         }).start();
     13 
     14         new Thread(() -> {
     15             num.loopC();
     16         }).start();
     17     }
     18 }
     19 
     20 class Num {
     21 
     22     private int i = 1;
     23     // 当前正在执行的线程的标记
     24     private int flag = 1;
     25     final private Lock lock = new ReentrantLock();
     26     final Condition conditionA = lock.newCondition();
     27     final Condition conditionB = lock.newCondition();
     28     final Condition conditionC = lock.newCondition();
     29 
     30     public void loopA() {
     31         while (true) {
     32             // 循环不停的抢锁
     33             lock.lock();
     34 
     35             try {
     36                 // 线程A判断是不是该自己打印
     37                 while (flag != 1) {
     38                     conditionA.await();
     39                 }
     40                 
     41                 // 唤醒线程B
     42                 // 注意这里:先唤醒B,再执行A的.
     43                 // 不要这两行代码放在下面的if中,最后会有线程出不来导致程序结束不了
     44                 conditionB.signal();
     45                 flag = 2;
     46 
     47                 if (i <= 99) {
     48                     System.out.println(Thread.currentThread().getName() + ":" + i);
     49                     i++;
     50 
     51                 } else {
     52                     break;
     53                 }
     54             } catch (InterruptedException e) {
     55                 e.printStackTrace();
     56             } finally {
     57                 lock.unlock();
     58             }
     59         }
     60     }
     61 
     62     // 同理
     63     public void loopB() {
     64         while (true) {
     65             lock.lock();
     66 
     67             try {
     68                 while (flag != 2) {
     69                     conditionB.await();
     70                 }
     71 
     72                 conditionC.signal();
     73                 flag = 3;
     74 
     75                 if (i <= 99) {
     76                     System.out.println(Thread.currentThread().getName() + ":" + i);
     77                     i++;
     78 
     79                 } else {
     80                     break;
     81                 }
     82             } catch (InterruptedException e) {
     83                 e.printStackTrace();
     84             } finally {
     85                 lock.unlock();
     86             }
     87         }
     88     }
     89 
     90     // 同理
     91     public void loopC() {
     92         while (true) {
     93             lock.lock();
     94 
     95             try {
     96                 while (flag != 3) {
     97                     conditionC.await();
     98                 }
     99 
    100                 conditionA.signal();
    101                 flag = 1;
    102 
    103                 if (i <= 99) {
    104                     System.out.println(Thread.currentThread().getName() + ":" + i);
    105                     i++;
    106 
    107                 } else {
    108                     break;
    109                 }
    110             } catch (InterruptedException e) {
    111                 e.printStackTrace();
    112             } finally {
    113                 lock.unlock();
    114             }
    115         }
    116     }
    117 }
    三个线程交替打印

    4、三个线程定制化打印问题

      开启 3 个线程,要求打印输出为 (A*3B*5C*7) * n。
      思想同理:接力棒A,交给B,B交给C,C交给A。有上一个问题对lock的使用,这个问题不难给出答案。
      代码示例:定制化打印

      1 public class Main {
      2 
      3     public static void main(String[] args) {
      4         Num num = new Num();
      5 
      6         new Thread(() -> {
      7             for (int i = 0; i < 10; i++) {
      8                 num.loopA();
      9             }
     10         }, "A").start();
     11 
     12         new Thread(() -> {
     13             for (int i = 0; i < 10; i++) {
     14                 num.loopB();
     15             }
     16         }, "B").start();
     17 
     18         new Thread(() -> {
     19             for (int i = 0; i < 10; i++) {
     20                 num.loopC();
     21             }
     22         }, "C").start();
     23     }
     24 }
     25 
     26 class Num {
     27     // 当前正在执行的线程的标记
     28     private int flag = 1;
     29     final private Lock lock = new ReentrantLock();
     30     final Condition conditionA = lock.newCondition();
     31     final Condition conditionB = lock.newCondition();
     32     final Condition conditionC = lock.newCondition();
     33 
     34     public void loopA() {
     35         lock.lock();
     36 
     37         try {
     38             // 线程A判断是不是该自己打印
     39             while (flag != 1) {
     40                 conditionA.await();
     41             }
     42             // 唤醒B
     43             conditionB.signal();
     44             flag = 2;
     45 
     46             // 将线程A的名称打印 3 遍
     47             for (int i = 0; i < 3; i++) {
     48                 System.out.println(Thread.currentThread().getName());
     49             }
     50         } catch (InterruptedException e) {
     51             e.printStackTrace();
     52         } finally {
     53             lock.unlock();
     54         }
     55     }
     56 
     57     // 同理
     58     public void loopB() {
     59         lock.lock();
     60 
     61         try {
     62             while (flag != 2) {
     63                 conditionB.await();
     64             }
     65 
     66             conditionC.signal();
     67             flag = 3;
     68 
     69             // 将线程B的名称打印 5 遍
     70             for (int i = 0; i < 5; i++) {
     71                 System.out.println(Thread.currentThread().getName());
     72             }
     73         } catch (InterruptedException e) {
     74             e.printStackTrace();
     75         } finally {
     76             lock.unlock();
     77         }
     78     }
     79 
     80     // 同理
     81     public void loopC() {
     82         lock.lock();
     83 
     84         try {
     85             while (flag != 3) {
     86                 conditionC.await();
     87             }
     88 
     89             conditionA.signal();
     90             flag = 1;
     91 
     92             // 将线程C的名称打印 7 遍
     93             for (int i = 0; i < 7; i++) {
     94                 System.out.println(Thread.currentThread().getName());
     95             }
     96         } catch (InterruptedException e) {
     97             e.printStackTrace();
     98         } finally {
     99             lock.unlock();
    100         }
    101     }
    102 }
    103 
    104 // 结果
    105 (AAABBBBBCCCCCCC)*10
    定制化打印

      这种定制化打印理解后,如果想要(ABC)*10,或其他形式的输出。相信修改哪里的参数应该很清楚了。

    二、生产者与消费者

    1、介绍

      生产者:不停生产产品,然后交给店员。
      消费者:不停消费产品,从店员处消费。
      店员:一次性持有的产品数量固定。

      代码示例:生产者生产20个,消费者消费20个,店员持有10个产品满。

     1 // 不写注释也能看懂的代码
     2 // 店员
     3 public class Clerk {
     4     // 产品数量
     5     private int product = 0;
     6 
     7     // 进货
     8     public synchronized void get() {
     9         if (product >= 10) {
    10             System.out.println("产品已满!");
    11         } else {
    12             System.out.println(Thread.currentThread().getName() + " : " + ++product);
    13         }
    14     }
    15 
    16     // 卖货
    17     public synchronized void sale() {
    18         if (product <= 0) {
    19             System.out.println("产品缺货!");
    20         } else {
    21             System.out.println(Thread.currentThread().getName() + " : " + --product);
    22         }
    23     }
    24 }
    25 
    26 // 生产者
    27 class Producer implements Runnable {
    28     private final Clerk clerk;
    29 
    30     public Producer(Clerk clerk) {
    31         this.clerk = clerk;
    32     }
    33 
    34     @Override
    35     public void run() {
    36         for (int i = 0; i < 20; i++) {
    37 //            try {
    38 //                Thread.sleep(200);
    39 //            } catch (InterruptedException e) {
    40 //            }
    41 
    42             clerk.get();
    43         }
    44     }
    45 }
    46 
    47 // 消费者
    48 class Consumer implements Runnable {
    49     private final Clerk clerk;
    50 
    51     public Consumer(Clerk clerk) {
    52         this.clerk = clerk;
    53     }
    54 
    55     @Override
    56     public void run() {
    57         for (int i = 0; i < 20; i++) {
    58             clerk.sale();
    59         }
    60     }
    61 }
     1 // 测试类
     2 public class Main {
     3     public static void main(String[] args) {
     4         Clerk clerk = new Clerk();
     5         Producer producer = new Producer(clerk);
     6         Consumer consumer = new Consumer(clerk);
     7         
     8         // 分别开启了一个生产者A 和 一个消费者B
     9         new Thread(producer, "生产者A").start();
    10         new Thread(consumer, "消费者B").start();
    11     }
    12 }
    13 
    14 // 可能的一种结果
    15 生产者A : 1
    16 消费者B : 0
    17 产品缺货!
    18 产品缺货!
    19 产品缺货!
    20 产品缺货!
    21 产品缺货!
    22 产品缺货!
    23 产品缺货!
    24 产品缺货!
    25 产品缺货!
    26 产品缺货!
    27 产品缺货!
    28 产品缺货!
    29 产品缺货!
    30 产品缺货!
    31 产品缺货!
    32 产品缺货!
    33 产品缺货!
    34 产品缺货!
    35 产品缺货!
    36 生产者A : 1
    37 生产者A : 2
    38 生产者A : 3
    39 生产者A : 4
    40 生产者A : 5
    41 生产者A : 6
    42 生产者A : 7
    43 生产者A : 8
    44 生产者A : 9
    45 生产者A : 10
    46 产品已满!
    47 产品已满!
    48 产品已满!
    49 产品已满!
    50 产品已满!
    51 产品已满!
    52 产品已满!
    53 产品已满!
    54 产品已满!

      理解:两个线程的共享变量是 product;两个线程共同竞争的锁,同步方法默认是this,指 clerk。
      这里没有使用等待唤醒机制。在生产满时,若抢到锁,依然会一直生产;在消费空时,若抢到锁,依然会一直消费。
      图解:

    2、等待唤醒

      上述结果并不是想要的。希望产品满时,等待消费者消费一个时,再生产;而产品空时,等待生产者生产一个时,再消费。用等待唤醒机制改进:

     1 public class Clerk {
     2     // 产品数量
     3     private int product = 0;
     4 
     5     // 进货
     6     public synchronized void get() {
     7         if (product >= 10) {
     8             System.out.println("产品已满!");
     9 
    10             // 满了就等待.就不生产
    11             try {
    12                 this.wait();
    13             } catch (InterruptedException e) {
    14                 e.printStackTrace();
    15             }
    16         } else {
    17             System.out.println(Thread.currentThread().getName() + " : " + ++product);
    18             // 通知消费者有货,可以消费
    19             this.notify();
    20         }
    21     }
    22 
    23     // 卖货
    24     public synchronized void sale() {
    25         if (product <= 0) {
    26             System.out.println("产品缺货!");
    27             
    28             // 缺货就等待
    29             try {
    30                 this.wait();
    31             } catch (InterruptedException e) {
    32                 e.printStackTrace();
    33             }
    34         } else {
    35             System.out.println(Thread.currentThread().getName() + " : " + --product);
    36             // 通知生产者,可以生产
    37             this.notify();
    38         }
    39     }
    40 }
    41 
    42 // 可能的一种结果
    43 生产者A : 1
    44 消费者B : 0
    45 产品缺货!
    46 生产者A : 1
    47 生产者A : 2
    48 消费者B : 1
    49 消费者B : 0
    50 产品缺货!
    51 生产者A : 1
    52 消费者B : 0
    53 产品缺货!
    54 生产者A : 1
    55 消费者B : 0
    56 产品缺货!
    57 生产者A : 1
    58 消费者B : 0
    59 产品缺货!
    60 生产者A : 1
    61 生产者A : 2
    62 消费者B : 1
    63 消费者B : 0
    64 产品缺货!
    65 生产者A : 1
    66 消费者B : 0
    67 产品缺货!
    68 生产者A : 1
    69 生产者A : 2
    70 生产者A : 3
    71 生产者A : 4
    72 消费者B : 3
    73 消费者B : 2
    74 消费者B : 1
    75 消费者B : 0
    76 生产者A : 1
    77 生产者A : 2
    78 生产者A : 3
    79 生产者A : 4
    80 生产者A : 5
    81 生产者A : 6
    82 生产者A : 7
    等待唤醒

      问题:如果将店员持有 10 个满改成持有 1 个满,如下:

    1 if (product >= 1) {}
    2 
    3 // 结果
    4 …………省略前面的
    5 生产者A : 1
    6 产品已满!
    7 消费者B : 0
    8 产品缺货!

      运行的结果没问题,但是程序停不下来。分析运行结果有利于更好的理解多线程编程。结合打印结果,不难得出:最后一次,消费者B缺货,等待,而生产者A执行完毕,已无法再唤醒消费者B。
      解决:把 else 打开即可。
      理解:其实不难理解它的现实语义。生产者A判断产品满,就等待,不满,就生产。消费者B判断产品空,就等待,不空,就消费。

    3、虚假唤醒问题

      问题:在上述代码基础上,如果有多个生产者,多个消费者,会出现负数。

     1 public class Main {
     2     public static void main(String[] args) {
     3         Clerk clerk = new Clerk();
     4         Producer producer = new Producer(clerk);
     5         Consumer consumer = new Consumer(clerk);
     6         new Thread(producer, "生产者A").start();
     7         new Thread(consumer, "消费者B").start();
     8         
     9         // 新增一个生产者和一个消费者
    10         new Thread(producer, "生产者C").start();
    11         new Thread(consumer, "消费者D").start();
    12     }
    13 }
    14 
    15 // 把上述 this.notify() 都改为 this.notifyAll();

      原因:消费者B抢到锁,product == 0,等待;消费者D抢到锁,product == 0,等待。然后,生产者A抢到锁,生产一个,product == 1。就会唤醒两个消费者,同时消费,就出现0、-1。这就是虚假唤醒问题。
      解决:把 if 改为 while 即可。

      参考文档:https://www.matools.com/api/java8

    4、用lock实现

      代码示例:完整用lock实现的生产者与消费者

     1 public class Clerk {
     2     // 产品数量
     3     private int product = 0;
     4     final private Lock lock = new ReentrantLock();
     5     final Condition condition = lock.newCondition();
     6 
     7     // 进货
     8     public void get() {
     9         lock.lock();
    10         try {
    11             while (product >= 1) {
    12                 System.out.println("产品已满!");
    13 
    14                 try {
    15                     condition.await();
    16                 } catch (InterruptedException e) {
    17                     e.printStackTrace();
    18                 }
    19             }
    20 
    21             System.out.println(Thread.currentThread().getName() + " : " + ++product);
    22             condition.signalAll();
    23         } finally {
    24             lock.unlock();
    25         }
    26     }
    27 
    28     // 卖货
    29     public void sale() {
    30         lock.lock();
    31         try {
    32             while (product <= 0) {
    33                 System.out.println("产品缺货!");
    34 
    35                 try {
    36                     condition.await();
    37                 } catch (InterruptedException e) {
    38                     e.printStackTrace();
    39                 }
    40             }
    41 
    42             System.out.println(Thread.currentThread().getName() + " : " + --product);
    43             condition.signalAll();
    44         } finally {
    45             lock.unlock();
    46         }
    47     }
    48 }
    49 
    50 // 生产者
    51 class Producer implements Runnable {
    52     private final Clerk clerk;
    53 
    54     public Producer(Clerk clerk) {
    55         this.clerk = clerk;
    56     }
    57 
    58     @Override
    59     public void run() {
    60         for (int i = 0; i < 20; i++) {
    61             try {
    62                 Thread.sleep(200);
    63             } catch (InterruptedException e) {
    64             }
    65 
    66             clerk.get();
    67         }
    68     }
    69 }
    70 
    71 // 消费者
    72 class Consumer implements Runnable {
    73     private final Clerk clerk;
    74 
    75     public Consumer(Clerk clerk) {
    76         this.clerk = clerk;
    77     }
    78 
    79     @Override
    80     public void run() {
    81         for (int i = 0; i < 20; i++) {
    82             clerk.sale();
    83         }
    84     }
    85 }
     1 // 测试类
     2 public class Main {
     3     public static void main(String[] args) {
     4         Clerk clerk = new Clerk();
     5         Producer producer = new Producer(clerk);
     6         Consumer consumer = new Consumer(clerk);
     7         new Thread(producer, "生产者A").start();
     8         new Thread(consumer, "消费者B").start();
     9 
    10         new Thread(producer, "生产者C").start();
    11         new Thread(consumer, "消费者D").start();
    12     }
    13 }

    作者:Craftsman-L

    本博客所有文章仅用于学习、研究和交流目的,版权归作者所有,欢迎非商业性质转载。

    如果本篇博客给您带来帮助,请作者喝杯咖啡吧!点击下面打赏,您的支持是我最大的动力!

  • 相关阅读:
    进程池和线程池
    GIL和互斥锁
    GIL全局解释器锁
    线程锁
    关于迭代器的一些总结
    python在linux上的GUI无法弹出界面
    import Tkinter的时候报错
    检查字符串中的结束标记
    关于模块的使用
    python中pip的安装
  • 原文地址:https://www.cnblogs.com/originator/p/15377455.html
Copyright © 2020-2023  润新知