簡介
多執行緒通訊一直是高頻面試考點,有些面試官可能要求現場手寫生產者/消費者程式碼來考察多執行緒的功底,今天我們以實際生活中母雞下蛋案例用程式碼剖析下實現過程。母雞在雞窩下蛋了,叫練從雞窩裡把雞蛋拿出來這個過程,母雞在雞窩下蛋,是生產者,叫練撿出雞蛋,叫練是消費者,一進一出就是執行緒中的生產者和消費者模型了,雞窩是放雞蛋容器。現實中還有很多這樣的案例,如醫院叫號。下面我們畫個圖表示下。
一對一生產和消費:一隻母雞和叫練
wait/notify
package com.duyang.thread.basic.waitLock.demo;
import java.util.ArrayList;
import java.util.List;
/**
* @author :jiaolian
* @date :Created in 2020-12-30 16:18
* @description:母雞下蛋:一對一生產者和消費者
* @modified By:
* 公眾號:叫練
*/
public class SingleNotifyWait {
//裝雞蛋的容器
private static class EggsList {
private static final List<String> LIST = new ArrayList();
}
//生產者:母雞實體類
private static class HEN {
private String name;
public HEN(String name) {
this.name = name;
}
//下蛋
public void proEggs() throws InterruptedException {
synchronized (EggsList.class) {
if (EggsList.LIST.size() == 1) {
EggsList.class.wait();
}
//容器新增一個蛋
EggsList.LIST.add("1");
//雞下蛋需要休息才能繼續產蛋
Thread.sleep(1000);
System.out.println(name+":下了一個雞蛋!");
//通知叫練撿蛋
EggsList.class.notify();
}
}
}
//人物件
private static class Person {
private String name;
public Person(String name) {
this.name = name;
}
//取蛋
public void getEggs() throws InterruptedException {
synchronized (EggsList.class) {
if (EggsList.LIST.size() == 0) {
EggsList.class.wait();
}
Thread.sleep(500);
EggsList.LIST.remove(0);
System.out.println(name+":從容器中撿出一個雞蛋");
//通知叫練撿蛋
EggsList.class.notify();
}
}
}
public static void main(String[] args) {
//創造一個人和一隻雞
HEN hen = new HEN("小黑");
Person person = new Person("叫練");
//建立執行緒執行下蛋和撿蛋的過程;
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen.proEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫練撿雞蛋的過程!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
person.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
如上面程式碼,我們定義EggsList類來裝雞蛋,HEN類表示母雞,Person類表示人。在主函式中建立母雞物件“小黑”,人物件“叫練”, 建立兩個執行緒分別執行下蛋和撿蛋的過程。程式碼中定義雞窩中最多隻能裝一個雞蛋(當然可以定義多個)。詳細過程:“小黑”母雞執行緒和“叫練”執行緒執行緒競爭鎖,如果“小黑”母雞執行緒先獲取鎖,發現EggsList雞蛋的個數大於0,表示有雞蛋,那就呼叫wait等待並釋放鎖給“叫練”執行緒,如果沒有雞蛋,就呼叫EggsList.LIST.add("1")表示生產了一個雞蛋並通知“叫練”來取雞蛋並釋放鎖讓“叫練”執行緒獲取鎖。“叫練”執行緒呼叫getEggs()方法獲取鎖後發現,如果雞窩中並沒有雞蛋就呼叫wait等待並釋放鎖通知“小黑”執行緒獲取鎖去下蛋,如果有雞蛋,說明“小黑”已經下蛋了,就把雞蛋取走,因為雞窩沒有雞蛋了,所以最後也要通知呼叫notify()方法通知“小黑”去下蛋,我們觀察程式的執行結果如下圖。兩個執行緒是死迴圈程式會一直執行下去,下蛋和撿蛋的過程中用到的鎖的是EggsList類的class,“小黑”和“叫練”競爭的都是統一把鎖,所以這個是同步的。這就是母雞“小黑”和“叫練”溝通的過程。
神馬???雞和人能溝通!!
Lock條件佇列
package com.duyang.thread.basic.waitLock.demo;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author :jiaolian
* @date :Created in 2020-12-30 16:18
* @description:母雞下蛋:一對一生產者和消費者 條件佇列
* @modified By:
* 公眾號:叫練
*/
public class SingleCondition {
private static Lock lock = new ReentrantLock();
//條件佇列
private static Condition condition = lock.newCondition();
//裝雞蛋的容器
private static class EggsList {
private static final List<String> LIST = new ArrayList();
}
//生產者:母雞實體類
private static class HEN {
private String name;
public HEN(String name) {
this.name = name;
}
//下蛋
public void proEggs() {
try {
lock.lock();
if (EggsList.LIST.size() == 1) {
condition.await();
}
//容器新增一個蛋
EggsList.LIST.add("1");
//雞下蛋需要休息才能繼續產蛋
Thread.sleep(1000);
System.out.println(name+":下了一個雞蛋!");
//通知叫練撿蛋
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
//人物件
private static class Person {
private String name;
public Person(String name) {
this.name = name;
}
//取蛋
public void getEggs() {
try {
lock.lock();
if (EggsList.LIST.size() == 0) {
condition.await();
}
Thread.sleep(500);
EggsList.LIST.remove(0);
System.out.println(name+":從容器中撿出一個雞蛋");
//通知叫練撿蛋
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
//創造一個人和一隻雞
HEN hen = new HEN("小黑");
Person person = new Person("叫練");
//建立執行緒執行下蛋和撿蛋的過程;
new Thread(()->{
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen.proEggs();
}
}).start();
//叫練撿雞蛋的過程!
new Thread(()->{
for (int i=0; i<Integer.MAX_VALUE;i++) {
person.getEggs();
}
}).start();
}
}
如上面程式碼,只是將synchronized換成了Lock,程式執行的結果和上面的一致,wait/notify換成了AQS的條件佇列Condition來控制執行緒之間的通訊。Lock需要手動加鎖lock.lock(),解鎖lock.unlock()的步驟放在finally程式碼塊保證鎖始終能被釋放。await底層是unsafe.park(false,0)呼叫C++程式碼實現。
多對多生產和消費:2只母雞和叫練/叫練媳婦
wait/notifyAll
package com.duyang.thread.basic.waitLock.demo;
import java.util.ArrayList;
import java.util.List;
/**
* @author :jiaolian
* @date :Created in 2020-12-30 16:18
* @description:母雞下蛋:多對多生產者和消費者
* @modified By:
* 公眾號:叫練
*/
public class MultNotifyWait {
//裝雞蛋的容器
private static class EggsList {
private static final List<String> LIST = new ArrayList();
}
//生產者:母雞實體類
private static class HEN {
private String name;
public HEN(String name) {
this.name = name;
}
//下蛋
public void proEggs() throws InterruptedException {
synchronized (EggsList.class) {
while (EggsList.LIST.size() >= 10) {
EggsList.class.wait();
}
//容器新增一個蛋
EggsList.LIST.add("1");
//雞下蛋需要休息才能繼續產蛋
Thread.sleep(1000);
System.out.println(name+":下了一個雞蛋!共有"+EggsList.LIST.size()+"個蛋");
//通知叫練撿蛋
EggsList.class.notify();
}
}
}
//人物件
private static class Person {
private String name;
public Person(String name) {
this.name = name;
}
//取蛋
public void getEggs() throws InterruptedException {
synchronized (EggsList.class) {
while (EggsList.LIST.size() == 0) {
EggsList.class.wait();
}
Thread.sleep(500);
EggsList.LIST.remove(0);
System.out.println(name+":從容器中撿出一個雞蛋!還剩"+EggsList.LIST.size()+"個蛋");
//通知叫練撿蛋
EggsList.class.notify();
}
}
}
public static void main(String[] args) {
//創造一個人和一隻雞
HEN hen1 = new HEN("小黑");
HEN hen2 = new HEN("小黃");
Person jiaolian = new Person("叫練");
Person wife = new Person("叫練媳婦");
//建立執行緒執行下蛋和撿蛋的過程;
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen1.proEggs();
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen2.proEggs();
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫練撿雞蛋的執行緒!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
jiaolian.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫練媳婦撿雞蛋的執行緒!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
wife.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
如上面程式碼,參照一對一生產和消費中wait/notify程式碼做了一些修改,建立了兩個母雞執行緒“小黑”,“小黃”,兩個撿雞蛋的執行緒“叫練”,“叫練媳婦”,執行結果是同步的,實現了多對多的生產和消費,如下圖所示。有如下幾點需要注意的地方:
- 雞窩中能容納最大的雞蛋是10個。
- 下蛋proEggs()方法中判斷雞蛋數量是否大於等於10個使用的是while迴圈,wait收到通知,喚醒當前執行緒,需要重新判斷一次,避免程式出現邏輯問題,這裡不能用if,如果用if,程式可能出現EggsList有超過10以上雞蛋的情況。這是這道程式中容易出現錯誤的地方,也是經常會被問到的點,值得重點探究下。
- 多對多的生產者和消費者。
Lock條件佇列
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author :jiaolian
* @date :Created in 2020-12-30 16:18
* @description:母雞下蛋:多對多生產者和消費者 條件佇列
* @modified By:
* 公眾號:叫練
*/
public class MultCondition {
private static Lock lock = new ReentrantLock();
//條件佇列
private static Condition condition = lock.newCondition();
//裝雞蛋的容器
private static class EggsList {
private static final List<String> LIST = new ArrayList();
}
//生產者:母雞實體類
private static class HEN {
private String name;
public HEN(String name) {
this.name = name;
}
//下蛋
public void proEggs() {
try {
lock.lock();
while (EggsList.LIST.size() >= 10) {
condition.await();
}
//容器新增一個蛋
EggsList.LIST.add("1");
//雞下蛋需要休息才能繼續產蛋
Thread.sleep(1000);
System.out.println(name+":下了一個雞蛋!共有"+ EggsList.LIST.size()+"個蛋");
//通知叫練/叫練媳婦撿蛋
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
//人物件
private static class Person {
private String name;
public Person(String name) {
this.name = name;
}
//取蛋
public void getEggs() throws InterruptedException {
try {
lock.lock();
while (EggsList.LIST.size() == 0) {
condition.await();
}
Thread.sleep(500);
EggsList.LIST.remove(0);
System.out.println(name+":從容器中撿出一個雞蛋!還剩"+ EggsList.LIST.size()+"個蛋");
//通知叫練撿蛋
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
//創造一個人和一隻雞
HEN hen1 = new HEN("小黑");
HEN hen2 = new HEN("小黃");
Person jiaolian = new Person("叫練");
Person wife = new Person("叫練媳婦");
//建立執行緒執行下蛋和撿蛋的過程;
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen1.proEggs();
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
hen2.proEggs();
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫練撿雞蛋的執行緒!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
jiaolian.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//叫練媳婦撿雞蛋的執行緒!
new Thread(()->{
try {
for (int i=0; i<Integer.MAX_VALUE;i++) {
wife.getEggs();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
如上面程式碼,只是將synchronized換成了Lock,程式執行的結果和上面的一致,下面我們比較下Lock和synchronized的異同。這個問題也是面試中會經常問到的!
Lock和synchronized比較
Lock和synchronized都能讓多執行緒同步。主要異同點表現如下!
- 鎖性質:Lock樂觀鎖是非阻塞的,底層是依賴cas+volatile實現,synchronized悲觀鎖是阻塞的,需要上下文切換。實現思想不一樣。
- 功能細節上:Lock需要手動加解鎖,synchronized自動加解鎖。Lock還提供顆粒度更細的功能,比如tryLock等。
- 執行緒通訊:Lock提供Condition條件佇列,一把鎖可以對應多個條件佇列,對執行緒控制更細膩。synchronized只能對應一個wait/notify。
主要就這些吧,如果對synchronized,volatile,cas關鍵字不太瞭解的童鞋,可以看看我之前的文章,有很詳細的案例和說明。
總結
今天用生活中的例子轉化成程式碼,實現了兩種多執行緒中消費者/生產者模式,給您的建議就是需要把程式碼敲一遍,如果認真執行了一遍程式碼應該能看明白,喜歡的請點贊加關注哦。我是叫練【公眾號】,邊叫邊練。