十、執行緒協作
阿新 • • 發佈:2021-08-02
一、生產者消費者問題
1. 問題
- 假設倉庫中只能放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中的產品取走消費
- 如果倉庫中沒有產品,則生產者將產品放入倉庫。否則停止生產並等待,直到倉庫中的產品被消費者取走為止
- 如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止
2. 分析
生產者和消費者共享同一個資源,並且兩者之間相互依賴,互為條件
- 對於生產者,沒有生產產品之前要通知消費者等待。而生產了產品之後又需要立即通知消費者
- 對於消費者,在消費之後要通知生產者已經結束消費,需要生產新的產品
- 在生產者消費者問題中,僅有
synchronized
是不夠的synchronized
synchronized
不能用來實現不同執行緒之間的通訊
二、執行緒通訊的方法
Java 中提供了幾個方法解決執行緒之間的通訊問題
方法名 | 作用 |
---|---|
wait() |
表示執行緒一致等待,直到其他執行緒通知。與sleep() 不同,會釋放鎖 |
wait(long timeout) |
指定等待的毫秒數 |
notify() |
喚醒一個處於等待狀態的執行緒 |
notifyAll() |
喚醒同一個物件上所有呼叫wait() 方法的執行緒,優先級別高的執行緒優先排程 |
注:以上均是 Object 類的方法,都只能在同步方法或者同步程式碼塊中使用,否則會丟擲IllegalMonitorStateExeception
異常
三、解決方式
1. 管程法
- 生產者:負責生產資料的模組(可能是方法、物件、執行緒、程序)
- 消費者:負責處理資料的模組(可能是方法、物件、執行緒、程序)
- 緩衝區:消費者不能直接使用生產者的資料,他們之間有個“緩衝區”
生產者將生產好的資料放入緩衝區,消費者從緩衝區拿出資料
例: public class TestPC { public static void main(String[] args) { SyncContainer container = new SyncContainer(); new Producer(container).start(); new Consumer(container).start(); } } class Producer extends Thread { SyncContainer container; public Producer(SyncContainer container) { this.container = container; } @Override public void run() { // 生產 for (int i = 0; i < 100; i++) { container.push(new Chicken(i)); System.out.println("生產了" + i + "只雞"); } } } class Consumer extends Thread { SyncContainer container; public Consumer(SyncContainer container) { this.container = container; } @Override public void run() { // 消費 for (int i = 0; i < 100; i++) { System.out.println("消費了第" + container.pop().id + "只雞"); } } } class Chicken { int id; public Chicken(int id) { this.id = id; } } class SyncContainer { // 需要一個容器大小 Chicken[] chickens = new Chicken[10]; // 容器計數器 int count = 0; // 生產者放入產品 public synchronized void push(Chicken c) { // 如果容器滿了,等待消費者消費 if (count == chickens.length) { // 通知消費者,生產者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 如果未滿,需要丟入產品 chickens[count] = c; count++; // 通知消費者可以消費了 this.notifyAll(); } // 消費者消費產品 public synchronized Chicken pop() { // 判斷能否消費 if (count == 0) { // 等待生產 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 如果可以消費 count--; Chicken chicken = chickens[count]; // 通知生產者生產 this.notifyAll(); return chicken; } }
2. 訊號燈法
藉助標誌位判斷,若為真消費者等待,若為假生產者等待
例:
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
// 生產者——演員
class Player extends Thread {
TV tv;
public Player(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.tv.play("快樂大本營");
} else {
this.tv.play("廣告");
}
}
}
}
// 消費者——觀眾
class Watcher extends Thread {
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
// 產品——節目
class TV {
// 演員錄製時,觀眾等待 T
// 觀眾觀看時,演員等待 F
// 錄製的節目
String show;
// 標誌位
boolean flag = true;
// 演員錄製
public synchronized void play(String show) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演員錄製了" + show);
// 通知觀眾觀看
this.notifyAll();
this.show = show;
this.flag = !this.flag;
}
// 觀眾觀看
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("觀眾觀看了" + show);
// 通知演員錄製
this.notifyAll();
this.flag = !this.flag;
}
}
四、小結
- 基於“鎖”的同步方式需要執行緒不斷地嘗試去獲得鎖,失敗了也需要繼續嘗試,這會很浪費資源,而等待/通知機制則能解決這一問題
- 等待/通知機制使用的是同一個物件鎖,如果兩個執行緒使用的是不同物件鎖,則不能用該機制通訊
五、擴充套件
訊號量
- 關鍵字
volatile
,能夠保證記憶體的可見性 - 使用
volatile
修飾的變數,如果在一個執行緒裡值發生了改變,那麼在其它執行緒中都是立即可見的 - 使用
volatile
修飾的變數需要進行原子操作