作業系統實驗之程序管理——生產者消費者問題
實驗要求
- 選擇一個經典的同步問題(生產者-消費者問題、讀者-寫者問題、哲學家就餐問題等),並模擬實現該同步問題的程序管理;
- 採用訊號量機制及相應的P操作、V操作;
- 應避免出現死鎖;
- 能夠顯示相關的狀態。
我這裡選擇的是生產者消費者問題,使用java實現
原始碼上傳到了本人github上
實驗原理
程式碼仿照某個博主的思想重寫的,本來想貼出來博主地址,但是忘了是哪位博主,如果日後找到了地址會再貼出來,實在抱歉。
程式碼結構
- 消費者Consumer作為消費者類,私有屬性有id、消費數、倉庫,方法有消費,消費實際是呼叫構造時傳入的倉庫中的消費方法。
- 生產者Producer作為生產者類,私有屬性有id、生產數、倉庫,方法有生產,生產實際是呼叫構造時傳入的倉庫中的生產方法。
- 倉庫Storage作為倉庫類,私有屬性有一個LinkedList作為商品存放的格子,Max_size是格子數。Storage封裝了消費者/生產者對倉庫取產品/存產品的方法。在這裡,LinkedList是本例中的公共區。
- 主類中例項化了倉庫,並用在例項化的時候將該倉庫傳給了消費者生產者,分別給消費者生產者設定了消費數量
核心程式碼
倉庫中消費方法
public void consume(int num, int id) {
synchronized (list) {
while (list.size() < num) {
System.out .println("【消費者" + id + "】預計取出產品數量:" + num + "\t當前庫存:"
+ list.size() + "\t產品太少,不夠消費。等待中……");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out .print("【消費者" + id + "】預計取出產品數量:" + num + "\n"
+ "【消費者" + id + "】正在取出中……");
//鎖住print輸出,防止被其他執行緒的輸出打斷
synchronized (System.out) {
for (int i = 1; i <= num; ++i) {
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.remove();
System.out.print(i + "...");
}
System.out.println();
}
list.notifyAll();
}
System.out.println("【消費者" + id + "】已取出:" + num + "\t當前庫存:" + list.size());
System.out.println("【消費者" + id + "】正在消費產品……");
try {
sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【消費者" + id + "】消費結束");
}
倉庫中生產方法
public void produce(int num, int id) {
System.out.println("【生產者" + id + "】正在生產產品……");
try {
sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【生產者" + id + "】生產結束");
synchronized (list) {
while (list.size() + num > MAX_SIZE) {
System.out.println("【生產者" + id + "】已生產產品:" + num + "\t當前庫存:"
+ list.size() + "\t倉庫已滿,不能存入");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("【生產者" + id + "】預計存入倉庫數量:" + num + "\n"
+ "【生產者" + id + "】正在存入中……");
//鎖住print輸出,防止被其他執行緒的輸出打斷
synchronized (System.out) {
for (int i = 1; i <= num; ++i) {
try {
sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Object());
System.out.print(i + "...");
}
System.out.println();
}
System.out.println("【生產者" + id + "】已存入:" + num + "\t當前庫存:" + list.size());
list.notifyAll();
}
}
程式碼思路
其實思路並不複雜,仔細看同步區的程式碼就會發現,它們基本滿足下面程式碼的格式。
synchronized(){
while(條件不滿足){
wait();//將執行緒掛起
}
執行某操作;//比如生產結束後將產品放入倉庫
notifyAll();//執行結束後將掛起的執行緒喚醒
}
synchronized(list)包裹起來的程式碼塊是同步程式碼段,含義簡單來說即當有執行緒訪問這一部分程式碼時(實際測試是訪問list物件時),會給其加鎖,那麼其他執行緒訪問list時會被阻塞,等待該執行緒結束list物件的訪問後才能繼續訪問。
而這裡while(條件不滿足)則是具體問題的條件,比如消費者想消費30個產品,首先倉庫裡得有30個產品才行吧?所以需要這裡有程式碼讓不滿足條件的執行緒掛起。
對應到作業系統裡講的pv原語,其實java關於pv原語的操作就是synchronized(list)和while欄位,我們對應著來看:首先,在作業系統裡關於list有一個訊號量是是否有人在操作倉庫,這個訊號量初始值是1,那麼synchronized(list)的左大括號和右大括號分別對應這個訊號量的p操作和v操作。其次,作業系統中關於倉庫的空間大小有限度,那麼倉庫當前大小這個訊號量是0,倉庫餘空間是100,這兩個訊號量的pv操作就對應了while部分。在作業系統中,作為生產者,開始拿出產品時需要首先對當前倉庫剩餘空間這個訊號量進行p操作,放一個商品就p操作一次,放完了一個商品就要對倉庫當前大小這個訊號量進行v操作,100就逐漸減小,0逐漸增大。這裡的p和v分別對應while部分,當100減到0的時候,沒有空間放商品了,while條件不再滿足,那麼該執行緒需要被掛起,生產者就得等待消費者先來消費。消費者是同理,這裡不予贅述。
另外我程式碼裡有些輸出也被synchronized包裹起來了,這裡解釋一下是因為print()方法是會被非同步執行的,也就是說我本來預定某個生產者/消費者隔0.3s輸出1…2…3…這樣模擬存入/取出的過程,如果在輸出的時候正好有某個消費者/生產者它生產/消費結束了,那麼它也要輸出一行話叫“生產結束”/“消費結束”,如果不包裹起來,輸出會被打斷,出現這樣尷尬的情況
【生產者2】正在存入中……1…2…3…4…5…6…7…8…【生產者1】生產結束
9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【生產者2】已存入:30 當前庫存:30
這裡就是生產者1發出其生產結束準備存入產品的訊息打斷了正在存入產品的生產者2的輸出,將這個輸出過程加上synchronized(System.out)幷包裹起來即可解決。(但是這樣產生的問題是生產者1生產完成後不能能及時的打印出自己生產完成了的訊息。嘛畢竟印表機只有一臺,只能這麼解決了)
程式碼結果
【生產者1】正在生產產品……
【生產者2】正在生產產品……
【生產者3】正在生產產品……
【生產者4】正在生產產品……
【消費者1】預計取出產品數量:50 當前庫存:0 產品太少,不夠消費。等待中……
【消費者2】預計取出產品數量:20 當前庫存:0 產品太少,不夠消費。等待中……
【消費者3】預計取出產品數量:30 當前庫存:0 產品太少,不夠消費。等待中……
【生產者2】生產結束
【生產者2】預計存入倉庫數量:30
【生產者2】正在存入中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【生產者2】已存入:30 當前庫存:30
【生產者1】生產結束
【消費者3】預計取出產品數量:30
【消費者3】正在取出中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【消費者3】已取出:30 當前庫存:0
【生產者3】生產結束
【生產者4】生產結束
【消費者2】預計取出產品數量:20 當前庫存:0 產品太少,不夠消費。等待中……
【消費者3】正在消費產品……
【消費者1】預計取出產品數量:50 當前庫存:0 產品太少,不夠消費。等待中……
【生產者4】預計存入倉庫數量:30
【生產者4】正在存入中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【消費者3】消費結束
【生產者4】已存入:30 當前庫存:30
【生產者3】預計存入倉庫數量:30
【生產者3】正在存入中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【生產者3】已存入:30 當前庫存:60
【生產者1】預計存入倉庫數量:30
【生產者1】正在存入中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…
【生產者1】已存入:30 當前庫存:90
【消費者1】預計取出產品數量:50
【消費者1】正在取出中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30…31…32…33…34…35…36…37…38…39…40…41…42…43…44…45…46…47…48…49…50…
【消費者1】已取出:50 當前庫存:40
【消費者1】正在消費產品……
【消費者2】預計取出產品數量:20
【消費者2】正在取出中……1…2…3…4…5…6…7…8…9…10…11…12…13…14…15…16…17…18…19…20…
【消費者2】已取出:20 當前庫存:20
【消費者2】正在消費產品……
【消費者1】消費結束
【消費者2】消費結束Process finished with exit code 0
馬後炮
除錯過程
這一部分記錄我自己在寫程式碼時踩到的坑。
關於synchronized到底同步的是啥,經過多方詢問以及自己測試,最終發現確實有些是synchronized包裹的程式碼段也會被非同步執行,所以synchronized同步的是括號中的物件,對於不是這個物件的操作是會被非同步執行的。假設我有句輸出在這個程式碼段裡,那麼它是可以被直接訪問到的,而不會被加鎖。也就是synchronized鎖是鎖在了括號裡的物件上。其實這一點也很好理解,一個方面來說這正好是pv原語的體現,pv原語只對訊號量操作,而不關注程式碼本身,而synchronized本身鎖住的是某個物件,而不是程式碼也正好能體現pv操作。另一個方面,在多執行緒處理的時候,本身就應該儘可能的減小鎖的粒度,不是同步所需要的萬不得已之時,儘可能的少去使用鎖,這樣才能加大效率。
存在的問題
其實這個程式碼的封裝程度不算高,我有意將對倉庫的存入和拿出操作寫在裡倉庫裡,不過問題在於沒有將存入和取出這兩個對倉庫的動作以及生產和消費這兩個消費者生產者本身的操作分開。而是通通放在了倉庫的消費、生產操作裡。其實我寫到了後面才發現,生產商品和把生產的東西放進倉庫應該要分開來寫,在程式碼結果裡可以看到我已經做了劃分,然而更好的解決方案應該是把生產的程式碼整個封裝到生產者類裡,把消費的程式碼整個封裝到消費者類裡。這樣倉庫類就是單純的做存入和取出操作,降低程式碼耦合度。
小結
在程序間通訊、synchronized方面查閱了許多資料才最終理解了java多執行緒的具體實現原理,最後對整個概念都有了進一步的認識。最有意思的是自己試著將作業系統的pv操作和java多執行緒的同步進行了比較和對應,其實可以認為java中synchronized等對pv原語的封裝實際也是為了簡化pv操作。