前言
生產(chǎn)者-消費者模式是一個經(jīng)典的多線程設(shè)計模式。
在生產(chǎn)者-消費者模式中剂习,通常有兩類線程蛮位,即若干個生產(chǎn)者和消費者線程。
- 生產(chǎn)者線程負(fù)責(zé)提交用戶請求
- 消費者線程負(fù)責(zé)處理生產(chǎn)者提交的任務(wù)鳞绕。
- 內(nèi)存緩沖區(qū) 緩存生產(chǎn)者提交的任務(wù)或數(shù)據(jù)失仁,供消費者使用。
開發(fā)需要解決的問題:
- 生產(chǎn)者線程與消費者線程對內(nèi)存緩沖區(qū)的操作的線程安全問題们何。
- 虛假喚醒萄焦。
測試:
/**
* 生產(chǎn)者與消費者案例。
* @author
*/
public class TestProductorAndConsumer {
public static void main(String[] args) {
//創(chuàng)建職員
Clerk clerk = new Clerk();
//創(chuàng)建生產(chǎn)者與消費者線程
Productor productor = new Productor(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(productor, "生產(chǎn)者1").start();
new Thread(consumer, "消費者1").start();
new Thread(productor, "生產(chǎn)者2").start();
new Thread(consumer, "消費者2").start();
}
}
// Clerk職員
class Clerk {
private int product = 0; //產(chǎn)品數(shù)量
private int capacity = 4; // 容量
// 進貨
public synchronized void get() {
if (product >= capacity) {
System.out.println("產(chǎn)品已滿冤竹!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + " : " + ++product);
this.notifyAll();
}
}
// 賣貨
public synchronized void sale() {
if (product <= 0) {
System.out.println("缺貨拂封!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + " : " + --product);
this.notifyAll();
}
}
}
// 生產(chǎn)者
class Productor implements Runnable {
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.get();
}
}
}
// 消費者
class Consumer implements Runnable {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
這就是一個簡單的生產(chǎn)者與消費者模型。
我們在Clerk類中給get()方法和sale()方法加了synchronized修飾符鹦蠕,來保證線程同步冒签。
但是運行后發(fā)現(xiàn)程序并沒有運行結(jié)束,分析發(fā)現(xiàn)钟病,我們的生產(chǎn)者線程最后沒有被喚醒萧恕,導(dǎo)致程序沒有結(jié)束刚梭。
對程序做一下修改:
/**
* 對Clerk類的get()與sale()方法做一點修改
*/
public synchronized void get() {
if (product >= capacity) {
System.out.println("產(chǎn)品已滿!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " : " + ++product);
this.notifyAll();
}
// 賣貨
public synchronized void sale() {
if (product <= 0) {
System.out.println("缺貨票唆!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " : " + --product);
this.notifyAll();
}
把notifyAll() 方法提到了else外面朴读,保證每個線程結(jié)束都能調(diào)用notifyAll()方法,運行一下惰说,發(fā)現(xiàn)程序確實能結(jié)束磨德,但是程序 product 變成了負(fù)數(shù)。這是由于調(diào)用notifyAll()喚醒了消費者模型吆视,執(zhí)行–product導(dǎo)致典挑。
我們來看一下wait()這個方法:
這就是我們要解決的虛假喚醒問題!@舶伞您觉!。
文檔提醒我們使用循環(huán)授滓。再對程序做一點修改
// 進貨
public synchronized void get() {
// 使用while防止虛假喚醒
while(product >= capacity) {
System.out.println("產(chǎn)品已滿!");
try {
// 在一個參數(shù)版本中般堆,中斷和虛假的喚醒是可能的在孝,這個方法應(yīng)該總是在循環(huán)中使用:
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 除非product<capacity,否則不執(zhí)行++product操作
System.out.println(Thread.currentThread().getName() + " : " + ++product);
this.notifyAll();
}
// 賣貨
public synchronized void sale() {
while (product <= 0) {
System.out.println("缺貨!");
try {
// 在一個參數(shù)版本中淮摔,中斷和虛假的喚醒是可能的私沮,這個方法應(yīng)該總是在循環(huán)中使用:
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 除非product>capacity,否則不執(zhí)行--product操作
System.out.println(Thread.currentThread().getName() + " : " + --product);
this.notifyAll();
}
將 if 改為 while 循環(huán)后,
生產(chǎn)者線程被喚醒后進行判斷:如果product >= capacity和橙,則繼續(xù)調(diào)用wait()等待仔燕,直到再次被喚醒。如果product < capacity, 則執(zhí)行++product魔招。
同理消費者線程被喚醒后也會進行判斷晰搀,不滿足條件會繼續(xù)等待,直到再次被喚醒办斑。滿足條件后處理任務(wù)外恕。
至此,我們的生產(chǎn)者-消費者模型就圓滿完成了乡翅。
我們再對程序做一點修改鳞疲,不使用synchronized來修飾方法,而是采用可重入鎖ReentrantLock來手動加鎖與釋放鎖峦朗。此時我們也就不能再使用wait()和notifyAll()方法了,因為這兩個方法synchronized關(guān)鍵字合作使用建丧。
此處我們需要使用Condition條件。
直接看代碼:
// 進貨
public void get() {
lock.lock();
try {
// 使用while防止虛假喚醒
while(product >= capacity) {
System.out.println("產(chǎn)品已滿波势!");
try {
// 在一個參數(shù)版本中翎朱,中斷和虛假的喚醒是可能的橄维,這個方法應(yīng)該總是在循環(huán)中使用:
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 除非product<capacity,否則不執(zhí)行++product操作
System.out.println(Thread.currentThread().getName() + " : " + ++product);
condition.signalAll();
}finally {
lock.unlock();
}
}
// 賣貨
public void sale() {
lock.lock();
try {
while (product <= 0) {
System.out.println("缺貨!");
try {
// 在一個參數(shù)版本中拴曲,中斷和虛假的喚醒是可能的争舞,這個方法應(yīng)該總是在循環(huán)中使用:
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 除非product>capacity,否則不執(zhí)行--product操作
System.out.println(Thread.currentThread().getName() + " : " + --product);
condition.signalAll();
}finally {
lock.unlock();
}
}
我們定義了一個可重入鎖 ReentrantLock lock, 通過lock.lock()來加鎖,通過lock.unlock()來釋放鎖澈灼,讓加鎖范圍更加靈活竞川。
這里提一下Condition接口提供的方法:
- await():方法會使當(dāng)前線程等待,同時釋放當(dāng)前鎖叁熔。當(dāng)其他線程使用signal()或signalAll()方法時委乌,線程會重新獲得鎖并繼續(xù)執(zhí)行。當(dāng)線程被中斷時荣回,也能跳出等待遭贸。
- await(long time,TimeUnit unit):方法會使線程等待,直到其他方法調(diào)用aignal()或者signalAll() 或者被中斷心软,或者等待超過設(shè)置的時間
- awaitUninterruptibly():方法與await()基本相同析桥,但它并不會在等待的時候響應(yīng)中斷蔫慧。
- signal():喚醒一個等待的線程。如果有線程正在等待此條件竭沫,則選擇一個線程進行喚醒爬泥。然后唱矛,該線程必須在從await返回之前重新獲取鎖误证。
- signalAll():喚醒所有等待的線程会放。如果有線程在這種情況下等待,那么它們將被喚醒胖秒。每個線程必須重新獲取鎖缎患,然后才能從await返回慕的。
最后
感謝你看到這里阎肝,文章有什么不足還請指正,覺得文章對你有幫助的話記得給我點個贊肮街,每天都會分享java相關(guān)技術(shù)文章或行業(yè)資訊风题,歡迎大家關(guān)注和轉(zhuǎn)發(fā)文章!