聊聊并发(二)——生产者与消费者
一、等待唤醒机制
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
本博客所有文章仅用于学习、研究和交流目的,版权归作者所有,欢迎非商业性质转载。
如果本篇博客给您带来帮助,请作者喝杯咖啡吧!点击下面打赏,您的支持是我最大的动力!