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

線程通信,在多線程系統中,不同的線程執行不同的任務;如果這些任務之間存在聯繫,那麼執行這些任務的線程之間就必須能夠通信,共同協調完成系統任務。

線程通信

生產者、消費者案例

案例分析

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

生產者、消費者案例

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

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

我們來對高耦合和低耦合做一個對比就會很直觀了:

  • 高(緊)耦合:生產者與消費者直接交互,生產者(蔬菜基地)把蔬菜直接給到給消費者,雙方之間的依賴程度很高;此時,生產者中就必須持有消費者對象的引用,同樣的道理,消費者也必須要持有生產者對象的引用;這樣,消費者和生產者才能夠直接交互。

  • 低(松)耦合: 引入一個中間對象——共享資源來,將生產者、消費者中需要對外輸出或者從外數據的操作封裝到中間對象中,這樣,消費者和生產者將會持有這個中間對象的引用,屏蔽了生產者和消費者直接的數據交互.,大大見減小生產者和消費者之間的依賴程度。

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

  • 集成顯卡普遍都集成於CPU中,所以如果集成顯卡出現了問題需要更換,那麼會連着CPU一塊更換,其維護成本與CPU其實是一樣的;

  • 獨立顯卡需要插在主板的顯卡接口上才能與計算機通信,其相對於整個計算機系統來說,是獨立的存在,即便出現問題需要更換,也只更換顯卡即可。

案例的代碼實現

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

蔬菜基地對象,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.java

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章