面試官:小夥子,你給我簡單說一下生產者與消費者模型吧

前言  

生產者-消費者模式是一個經典的多線程設計模式。
       在生產者-消費者模式中,一般有兩類線程,即若干個生產者和消費者線程。java

  • 生產者線程負責提交用戶請求
  • 消費者線程負責處理生產者提交的任務。
  • 內存緩衝區 緩存生產者提交的任務或數據,供消費者使用。

開發須要解決的問題:

  1. 生產者線程與消費者線程對內存緩衝區的操做的線程安全問題。
  2. 虛假喚醒。

測試:設計模式

/**
 * 生產者與消費者案例。
 * @author 
 */
public class TestProductorAndConsumer {

    public static void main(String[] args) {
        //建立職員
        Clerk clerk = new Clerk();

        //建立生產者與消費者線程
        Productor productor = new Productor(clerk);
        Consumer consumer = new Consumer(clerk);

        new Thread(productor, "生產者1").start();
        new Thread(consumer, "消費者1").start();
        new Thread(productor, "生產者2").start();
        new Thread(consumer, "消費者2").start();
    }
}

// Clerk職員
class Clerk {
    private int product = 0;    //產品數量
    private int capacity = 4;   // 容量
    // 進貨

    public synchronized void get() {

        if (product >= capacity) {
            System.out.println("產品已滿!");

            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();
        }
    }
}

// 生產者
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();
        }
    }
}

這就是一個簡單的生產者與消費者模型。
       咱們在Clerk類中給get()方法和sale()方法加了synchronized修飾符,來保證線程同步。緩存

 可是運行後發現程序並無運行結束,分析發現,咱們的生產者線程最後沒有被喚醒,致使程序沒有結束。安全

對程序作一下修改:

/**
 * 對Clerk類的get()與sale()方法作一點修改
 */
public synchronized void get() {
        if (product >= capacity) {
            System.out.println("產品已滿!");

            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外面,保證每一個線程結束都能調用notifyAll()方法,運行一下,發現程序確實能結束,可是程序 product 變成了負數。這是因爲調用notifyAll()喚醒了消費者模型,執行–product致使。多線程

咱們來看一下wait()這個方法:
ide

這就是咱們要解決的虛假喚醒問題!!!。
文檔提醒咱們使用循環。再對程序作一點修改測試

// 進貨
    public synchronized void get() {

        // 使用while防止虛假喚醒
        while(product >= capacity) {
            System.out.println("產品已滿!");

            try {
                // 在一個參數版本中,中斷和虛假的喚醒是可能的,這個方法應該老是在循環中使用:
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 除非product<capacity,不然不執行++product操做
        System.out.println(Thread.currentThread().getName() + " : " + ++product);
        this.notifyAll();

    }

    // 賣貨
    public synchronized void sale() {
        while (product <= 0) {
            System.out.println("缺貨!");

            try {
                // 在一個參數版本中,中斷和虛假的喚醒是可能的,這個方法應該老是在循環中使用:
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 除非product>capacity,不然不執行--product操做
        System.out.println(Thread.currentThread().getName() + " : " + --product);
        this.notifyAll();
    }

將 if 改成 while 循環後,
       生產者線程被喚醒後進行判斷:若是product >= capacity,則繼續調用wait()等待,直到再次被喚醒。若是product < capacity, 則執行++product。
       同理消費者線程被喚醒後也會進行判斷,不知足條件會繼續等待,直到再次被喚醒。知足條件後處理任務。this

至此,咱們的生產者-消費者模型就圓滿完成了。spa

  咱們再對程序作一點修改,不使用synchronized來修飾方法,而是採用可重入鎖ReentrantLock來手動加鎖與釋放鎖。此時咱們也就不能再使用wait()和notifyAll()方法了,由於這兩個方法synchronized關鍵字合做使用。
此處咱們須要使用Condition條件。線程

直接看代碼:

// 進貨
    public void get() {
        lock.lock();
        try {
            // 使用while防止虛假喚醒
            while(product >= capacity) {
                System.out.println("產品已滿!");
                try {
                    // 在一個參數版本中,中斷和虛假的喚醒是可能的,這個方法應該老是在循環中使用:
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 除非product<capacity,不然不執行++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 {
                    // 在一個參數版本中,中斷和虛假的喚醒是可能的,這個方法應該老是在循環中使用:
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 除非product>capacity,不然不執行--product操做
            System.out.println(Thread.currentThread().getName() + " : " + --product);

            condition.signalAll();
        }finally {
            lock.unlock();
        }

    }

 咱們定義了一個可重入鎖 ReentrantLock lock, 經過lock.lock()來加鎖,經過lock.unlock()來釋放鎖,讓加鎖範圍更加靈活。

這裏提一下Condition接口提供的方法:

  • await():方法會使當前線程等待,同時釋放當前鎖。當其餘線程使用signal()或signalAll()方法時,線程會從新得到鎖並繼續執行。當線程被中斷時,也能跳出等待。
  • await(long time,TimeUnit unit):方法會使線程等待,直到其餘方法調用aignal()或者signalAll() 或者被中斷,或者等待超過設置的時間
  • awaitUninterruptibly():方法與await()基本相同,但它並不會在等待的時候響應中斷。
  • signal():喚醒一個等待的線程。若是有線程正在等待此條件,則選擇一個線程進行喚醒。而後,該線程必須在從await返回以前從新獲取鎖。
  • signalAll():喚醒全部等待的線程。若是有線程在這種狀況下等待,那麼它們將被喚醒。每一個線程必須從新獲取鎖,而後才能從await返回。

最後

感謝你看到這裏,文章有什麼不足還請指正,以爲文章對你有幫助的話記得給我點個贊,天天都會分享java相關技術文章或行業資訊,歡迎你們關注和轉發文章!

相關文章
相關標籤/搜索