今天看了一片博文,講Java多線程之線程的協作,其中作者用程序實例說明了生產者和消費者問題,但我及其他讀者發現程序多跑幾次還是會出現死鎖,百度搜了下大都數的例子也都存在bug,經過仔細研究發現其中的問題,並解決了,感覺有意義貼出來分享下。
下面首先貼出的是有bug的代碼,一個4個類,Plate.java:
package CreatorAndConsumer;
import java.util.ArrayList;
import java.util.List;
/**
* 盤子,表示共享的資源
* @author Martin
*
*/
public class Plate {
private List<Object> eggs = new ArrayList<Object>();
/**
* 獲取蛋
* @return
*/
public Object getEgg()
{
System.out.println("消費者取蛋");
Object egg = eggs.get(0);
eggs.remove(0);
return egg;
}
/**
* 加入蛋
* @return
*/
public void addEgg(Object egg)
{
System.out.println("生產者生蛋");
eggs.add(egg);
}
/**
* 獲取蛋個數
* @return
*/
public int getEggNum()
{
return eggs.size();
}
}
消費者類:Consumer2.java
package CreatorAndConsumer;
public class Consumer2 implements Runnable {
/**
* 線程資源
*/
private Plate plate;
public Consumer2(Plate plate) {
this.plate = plate;
}
@Override
public void run() {
synchronized (plate) {
// 如果此時蛋的個數大於0,則等等
while (plate.getEggNum() < 1) {
try {
// 這個細節需要注意,如果線程進入wait,那麼其上的鎖就會暫時得到釋放,
// 不然其他線程也不能進行加鎖,然後喚醒本線程
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 喚醒後,再次得到資源鎖,且條件滿足就可以放心地取蛋了
plate.getEgg();
plate.notify();
}
}
}
生產者類:Creator2.java
package CreatorAndConsumer;
/**
* 生產者
*
* @author Martin
*
*/
public class Creator2 implements Runnable {
/**
* 線程資源
*/
private Plate plate;
public Creator2(Plate plate) {
this.plate = plate;
}
@Override
public void run() {
synchronized (plate) {
// 如果此時蛋的個數大於0,則等等
while (plate.getEggNum() >= 5) {
try {
// 這個細節需要注意,如果線程進入wait,那麼其上的鎖就會暫時得到釋放,
// 不然其他線程也不能進行加鎖,然後喚醒本線程
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 喚醒後,再次得到資源鎖,且條件滿足就可以放心地生蛋啦
Object egg = new Object();
plate.addEgg(egg);
plate.notify();
}
}
}
測試類:Tester.java
package CreatorAndConsumer;
public class Tester {
public static void main(String[] args)
{
//共享資源
Plate plate = new Plate();
//添加生產者和消費者
for(int i = 0 ; i < 100; i ++)
{
//有bug版
new Thread(new Creator2(plate)).start();
new Thread(new Consumer2(plate)).start();
//無bug版
//new Thread(new Creator(plate)).start();
//new Thread(new Consumer(plate)).start();
}
}
}
如果多運行幾次或者將測試類中的循環次數改大,則會發現出現死鎖的概率還是很高的。下面分析發生這種問題的原因:
在jdk中對於Object.wait有這樣的一段解釋:當前線程必須擁有此對象監視器。該線程放棄對此監視器的所有權並等待,直到其他線程通過調用 notify 方法,或 notifyAll 方法通知在此對象的監視器上等待的線程醒來。可見下面一種情況就可能出現:消費者1進入等待狀態(此時資源鎖已被放開),如果此時消費者2獲取到了資源同步鎖(沒有人保證消費者1進入等待,下一個拿到鎖的一定是生產者),消費者2判斷沒有資源也進入等待狀態;此時生產者1生產了,並notify了消費者1,消費者1順利地消費了,並執行notify操作,但此時消費者2卻也因爲資源而處於等待狀態,從而喚醒了消費者2(消費者1本欲喚醒其他生產者),而此時並沒有任何資源,導致了整個程序因爲消費者2陷入無限的等待,形成了死鎖。
經過以上分析,究其根本原因是:同時幾個消費者或幾個生產者處於等待狀態,導致消費者可能喚醒的還是消費者,或者生產者喚醒的還是生產者。那麼如果我們能夠保證同時只有一個消費者處於wait狀態(生產者同理),那就就能保證消費者喚醒的一定是生產者,從而能使整個任務順利進行下去。下面是修改後的代碼:
改進後的Consumer.java
package CreatorAndConsumer;
public class Consumer implements Runnable {
/**
* 線程資源
*/
private Plate plate;
/**
* 生產者鎖:用於鎖定同一時間只能有一個生產者進入生產臨界區(如果同時又兩個生產者進入臨界區,那麼很有可能其中一個生產者本想喚醒消費者卻喚醒了生產者)
*/
private static Object consumerLocker = new Object();
public Consumer(Plate plate) {
this.plate = plate;
}
@Override
public void run() {
// 必須先獲得生產者鎖才能生產
synchronized (consumerLocker) {
synchronized (plate) {
// 如果此時蛋的個數大於0,則等等
while (plate.getEggNum() < 1) {
try {
// 這個細節需要注意,如果線程進入wait,那麼其上的鎖就會暫時得到釋放,
// 不然其他線程也不能進行加鎖,然後喚醒本線程
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 喚醒後,再次得到資源鎖,且條件滿足就可以放心地取蛋了
plate.getEgg();
plate.notify();
}
}
}
}
改進後的Creator.java:
package CreatorAndConsumer;
/**
* 生產者
*
* @author Martin
*
*/
public class Creator implements Runnable {
/**
* 線程資源
*/
private Plate plate;
/**
* 生產者鎖:用於鎖定同一時間只能有一個生產者進入生產臨界區(如果同時又兩個生產者進入臨界區,那麼很有可能其中一個生產者本想喚醒消費者卻喚醒了生產者)
*/
private static Object creatorLocker = new Object();
public Creator(Plate plate) {
this.plate = plate;
}
@Override
public void run() {
//必須先獲得生產者鎖才能生產
synchronized (creatorLocker) {
synchronized (plate) {
// 如果此時蛋的個數大於0,則等等
while (plate.getEggNum() >= 5) {
try {
// 這個細節需要注意,如果線程進入wait,那麼其上的鎖就會暫時得到釋放,
// 不然其他線程也不能進行加鎖,然後喚醒本線程
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 喚醒後,再次得到資源鎖,且條件滿足就可以放心地生蛋啦
Object egg = new Object();
plate.addEgg(egg);
plate.notify();
}
}
}
}
改進說明:改進後的生產者和消費者分別加了生產者鎖和消費者鎖,分別用於鎖定同一時間只能有一個消費者(生產者)進入生產臨界區(如果同時又兩個生產者進入臨界區,那麼很有可能其中一個生產者本想喚醒消費者卻喚醒了生產者),總的來說就是一共三個鎖:消費者鎖、生產者鎖、生產者和消費者共享的鎖。
最終,寫多線程的時候需要注意的是一個資源可能喚醒的是所有因該資源而等待的線程,因此消費者線程不一定喚醒的就是生產者線程也可能是消費者線程。