經過實現生產者、消費者再次案例實踐Java 多線程

線程通訊,在多線程系統中,不一樣的線程執行不一樣的任務;若是這些任務之間存在聯繫,那麼執行這些任務的線程之間就必須可以通訊,共同協調完成系統任務。java

線程通訊

生產者、消費者案例

案例分析編程

在案例中明,蔬菜基地做爲生產者,負責生產蔬菜,並向超市輸送生產的蔬菜;消費者經過向超市購買得到蔬菜;超市怎做爲生產者和消費者之間的共享資源,都會和超市有聯繫;蔬菜基地、共享資源、消費者之間的交互流程以下:segmentfault

生產者、消費者案例

在這個案例中,爲何不設計成生產者直接與給消費者交互?讓二者直接交換數據不是更好嗎,選擇先先把數據存儲到共享資源中,而後消費者再從共享資源中取出數據使用,中間多了一個環節不是更麻煩了?安全

其實不是的,設計成這樣是有緣由的,由於這樣設計很好的體現了面向對象的低耦合的設計理念;經過這樣實現的程序能更加符合人的操做理念,更加貼合現實環境;同時,也能很好的避免因生產者與消費者直接交互而致使的操做不安全的問題。網絡

咱們來對高耦合和低耦合作一個對比就會很直觀了:多線程

  • 高(緊)耦合:生產者與消費者直接交互,生產者(蔬菜基地)把蔬菜直接給到給消費者,雙方之間的依賴程度很高;此時,生產者中就必須持有消費者對象的引用,一樣的道理,消費者也必需要持有生產者對象的引用;這樣,消費者和生產者纔可以直接交互。
  • 低(鬆)耦合: 引入一個中間對象——共享資源來,將生產者、消費者中須要對外輸出或者從外數據的操做封裝到中間對象中,這樣,消費者和生產者將會持有這個中間對象的引用,屏蔽了生產者和消費者直接的數據交互.,大大見減少生產者和消費者之間的依賴程度。

關於高耦合和低耦合的區別,電腦中主機中的集成顯卡和獨立顯卡也是一個很是好的例子。異步

  • 集成顯卡廣泛都集成於CPU中,因此若是集成顯卡出現了問題須要更換,那麼會連着CPU一塊更換,其維護成本與CPU實際上是同樣的;
  • 獨立顯卡須要插在主板的顯卡接口上才能與計算機通訊,其相對於整個計算機系統來講,是獨立的存在,即使出現問題須要更換,也只更換顯卡便可。

案例的代碼實現

接下來咱們使用多線程技術實現該案例,案例代碼以下:ide

蔬菜基地對象,VegetableBase.java性能

// VegetableBase.java

// 蔬菜基地
public class VegetableBase implements Runnable {

    // 超市實例
    private Supermarket supermarket = null;

    public VegetableBase(Supermarket supermarket) {
        this.supermarket = supermarket;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                supermarket.push("黃瓜", 1300);
                System.out.println("push : 黃瓜 " + 1300);
            } else {
                supermarket.push("青菜", 1400);
                System.out.println("push : 青菜 " + 1400);
            }
        }
    }
}

消費者對象,Consumer.javathis

// Consumer.java

// 消費者
public class Consumer implements Runnable {

    // 超市實例
    private Supermarket supermarket = null;

    public Consumer(Supermarket supermarket) {
        this.supermarket = supermarket;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            supermarket.popup();
        }
    }
}

超市對象,Supermarket.java

// Supermarket.java

// 超市
public class Supermarket {

    // 蔬菜名稱
    private String name;
    // 蔬菜數量
    private Integer num;

    // 蔬菜基地想超市輸送蔬菜
    public void push(String name, Integer num) {
        this.name = name;
        this.num = num;
    }

    // 用戶從超市中購買蔬菜
    public void popup() {
        // 爲了讓效果更明顯,在這裏模擬網絡延遲
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {

        }
        System.out.println("蔬菜:" + this.name + ", " + this.num + "顆。");
    }

}

運行案例,App.java

// 案例應用入口
public class App {

    public static void main(String[] args) {
        // 建立超市實例
        Supermarket supermarket = new Supermarket();
        // 蔬菜基地線程啓動, 開始往超市輸送蔬菜
        new Thread(new VegetableBase(supermarket)).start();
        new Thread(new VegetableBase(supermarket)).start();
        // 消費者線程啓動,消費者開始購買蔬菜
        new Thread(new Consumer(supermarket)).start();
        new Thread(new Consumer(supermarket)).start();
    }

}

發現了問題

運行該案例,打印出運行結果,外表一片祥和,可仍是被敏銳的發現了問題,問題以下所示:

案例運行中發現的問題

在一片看似祥和的打印結果中,出現了一個很不祥和的特例,生產基地在輸送蔬菜時,黃瓜的數量一直都是1300顆,青菜的數量一直是1400顆,可是在消費者消費時卻出現了蔬菜名稱是黃瓜的,但數量倒是青菜的數量的狀況。

之因此出現這樣的問題,是由於在本案例共享的資源中,多個線程共同競爭資源時沒有使用同步操做,而是異步操做,今兒致使了資源分配紊亂的狀況;須要注意的是,並非由於咱們在案例中使用Thread.sleep();模擬網絡延遲才致使問題出現,而是原本就存在問題,使用Thread.sleep();只是讓問題更加明顯。

案例問題的解決

在本案例中須要解決的問題有兩個,分別以下:

  1. 問題一: 蔬菜名稱和數量不匹配的問題。
  2. 問題二: 須要保證超市無貨時生產,超市有貨時才消費。

針對問題一解決方案:保證蔬菜基地在輸送蔬菜的過程保持同步,中間不能被其餘線程(特別是消費者線程)干擾,打亂輸送操做;直至當前線程完成輸送後,其餘線程才能進入操做,一樣的,當有線程進入操做後,其餘線程只能在操做外等待。

因此,技術方案可使用同步代碼塊/同步方法/Lock機制來保持操做的同步性。

針對問題二的解決方案:給超市一個有無貨的狀態標誌,

  • 超市無貨時,蔬菜基地輸送蔬菜補貨,此時生產基地線程可操做;
  • 超市有貨時,消費者線程可操做;就是:保證生產基地 ——> 共享資源 ——> 消費者這個整個流程的完整運行。

技術方案:使用線程中的等待和喚醒機制

同步操做,分爲同步代碼塊同步方法兩種。詳情可查看個人另一篇關於多線程的文章:「JAVA」Java 線程不安全分析,同步鎖和Lock機制,哪一個解決方案更好

  1. 在同步代碼塊中的同步鎖必須選擇多個線程共同的資源對象,當前生產者線程在生產數據的時候(先擁有同步鎖),其餘線程就在鎖池中等待獲取鎖;當生產者線程執行完同步代碼塊的時候,就會釋放同步鎖,其餘線程開始搶鎖的使用權,搶到後就會擁有該同步鎖,執行完成後釋放,其餘線程再開始搶鎖的使用權,依次往復執行。
  2. 多個線程只有使用同一個對象(就比如案例中的共享資源對象)的時候,多線程之間纔有互斥效果,咱們把這個用來作互斥的對象稱之爲同步監聽對象,又稱同步監聽器、互斥鎖、同步鎖,同步鎖是一個抽象概念,能夠理解爲在對象上標記了一把鎖。
  3. 同步鎖對象能夠選擇任意類型的對象便可,只須要保證多個線程使用的是相同鎖對象便可。在任什麼時候候,最多隻能運行一個線程擁有同步鎖。由於只有同步監聽鎖對象才能調用waitnotify方法,waitnotify方法存在於Object類中。

線程通訊之 wait和notify方法

java.lang.Object 中提供了用於操做線程通訊的方法,詳情以下:

  • wait()執行該方法的線程對象會釋放同步鎖,而後JVM把該線程存放到等待池中,等待着其餘線程來喚醒該線程;
  • notify()執行該方法的線程會喚醒在等待池中處於等待狀態的的任意一個線程,把線程轉到同步鎖池中等待;
  • notifyAll()執行該方法的線程會喚醒在等待池中處於等待狀態的全部的線程,把這些線程轉到同步鎖池中等待;

注意:上述方法只能被同步監聽鎖對象來調用,不然發生 IllegalMonitorStateException

wait和notify方法應用實例

假設 A線程B線程共同操做一個X對象(同步鎖) ,A、B線程能夠經過X對象waitnotify方法來進行通訊,流程以下:

  1. A線程執行X對象的同步方法時,A線程持有X對象的鎖,B線程沒有執行機會,此時的B線程會在X對象的鎖池中等待;
  2. A線程在同步方法中執行X.wait()方法時,A線程會釋放X對象的同步鎖,而後進入X對象的等待池中;
  3. 接着,在X對象的鎖池中等待鎖的B線程獲取X對象的鎖,執行X的另外一個同步方法;
  4. B線程在同步方法中執行X.notify()方法時,JVM會把A線程X對象的等待池中轉到X對象的同步鎖池中,等待獲取鎖的使用權;
  5. B線程執行完同步方法後,會釋放擁有的鎖,而後A線程得到鎖,繼續執行同步方法;

基於上述機制,咱們就可使用同步操做 + wait和notify方法來解決案例中的問題了,從新來實現共享資源——超市對象:

// 超市
public class Supermarket {

    // 蔬菜名稱
    private String name;
    // 蔬菜數量
    private Integer num;
      // 超市是否爲空
      private Boolean isEmpty = true;

    // 蔬菜基地向超市輸送蔬菜
    public synchronized void push(String name, Integer num) {
          try {
              while (!isEmpty) {   // 超市有貨時,再也不輸送蔬菜,而是要等待消費者獲取
                   this.wait();  
             }
                this.name = name;
                this.num = num;
              isEmpty = false;
              this.notify();                 // 喚醒另外一個線程
        } catch(Exception e) {
            
        }
        
    }

    // 用戶從超市中購買蔬菜
    public synchronized void popup() {
        
        try {
              while (isEmpty) { // 超市無貨時,再也不提供消費,而是要等待蔬菜基地輸送
                   this.wait();
            }
              // 爲了讓效果更明顯,在這裏模擬網絡延遲
            Thread.sleep(1000);
              System.out.println("蔬菜:" + this.name + ", " + this.num + "顆。");
              isEmpty = true;
              this.notify();  // 喚醒另外一線程
        } catch (Exception e) {

        }   
    }
}

線程通訊之 使用Lock和Condition接口

因爲waitnotify方法,只能被同步監聽鎖對象來調用,不然發生
IllegalMonitorStateException。從Java 5開始,提供了Lock機制 ,同時還有處理Lock機制的通訊控制的Condition接口Lock機制沒有同步鎖的概念,也就沒有自動獲取鎖和自動釋放鎖的這樣的操做了。

由於沒有同步鎖,因此Lock機制中的線程通訊就不能調用waitnotify方法了;一樣的,Java 5 中也提供瞭解決方案,所以從Java 5開始,能夠:

  1. 使用Lock機制取代synchronized 代碼塊synchronized 方法;
  2. 使用Condition接口對象的await、signal、signalAll方法取代Object類中的wait、notify、notifyAll方法;

Lock和Condition接口的性能也比同步操做要高不少,因此這種方式也是咱們推薦使用的方式。

咱們可使用Lock機制和Condition接口方法來解決案例中的問題,從新來實現的共享資源——超市對象,代碼以下:

// 超市
public class Supermarket {

    // 蔬菜名稱
    private String name;
    // 蔬菜數量
    private Integer num;
      // 超市是否爲空
      private Boolean isEmpty = true;
        // lock
        private final Lock lock = new ReentrantLock();
        // Condition
        private Condition condition = lock.newCondition();
        

    // 蔬菜基地向超市輸送蔬菜
    public synchronized void push(String name, Integer num) {
          lock.lock(); // 獲取鎖
          try {
              while (!isEmpty) {   // 超市有貨時,再也不輸送蔬菜,而是要等待消費者獲取
                   condition.await();  
             }
                this.name = name;
                this.num = num;
              isEmpty = false;
              condition.signalAll();                 
        } catch(Exception e) {
            
        } finally {
                lock.unlock();  // 釋放鎖
        }
        
    }

    // 用戶從超市中購買蔬菜
    public synchronized void popup() {
        lock.lock();
        try {
              while (isEmpty) { // 超市無貨時,再也不提供消費,而是要等待蔬菜基地輸送
                   condition.await();
            }
              // 爲了讓效果更明顯,在這裏模擬網絡延遲
            Thread.sleep(1000);
              System.out.println("蔬菜:" + this.name + ", " + this.num + "顆。");
              isEmpty = true;
              condition.signalAll();  
        } catch (Exception e) {
                
        }   finally {
                lock.unlock();
        }
    }
}

完結,老夫雖不正經,但老夫一身的才華!關注我,獲取更多編程科技知識。

相關文章
相關標籤/搜索