「JAVA」通過實(shí)現(xiàn)生產(chǎn)者、消費(fèi)者案例再次實(shí)踐Java 多線程

線程通信,在多線程系統(tǒng)中,不同的線程執(zhí)行不同的任務(wù);如果這些任務(wù)之間存在聯(lián)系,那么執(zhí)行這些任務(wù)的線程之間就必須能夠通信,共同協(xié)調(diào)完成系統(tǒng)任務(wù)。

線程通信

生產(chǎn)者、消費(fèi)者案例

案例分析

在案例中明,蔬菜基地作為生產(chǎn)者,負(fù)責(zé)生產(chǎn)蔬菜,并向超市輸送生產(chǎn)的蔬菜;消費(fèi)者通過向超市購買獲得蔬菜;超市怎作為生產(chǎn)者和消費(fèi)者之間的共享資源,都會和超市有聯(lián)系;蔬菜基地、共享資源、消費(fèi)者之間的交互流程如下:

生產(chǎn)者、消費(fèi)者案例

在這個(gè)案例中,為什么不設(shè)計(jì)成生產(chǎn)者直接與給消費(fèi)者交互?讓兩者直接交換數(shù)據(jù)不是更好嗎,選擇先先把數(shù)據(jù)存儲到共享資源中,然后消費(fèi)者再從共享資源中取出數(shù)據(jù)使用,中間多了一個(gè)環(huán)節(jié)不是更麻煩了?

其實(shí)不是的,設(shè)計(jì)成這樣是有原因的,因?yàn)檫@樣設(shè)計(jì)很好的體現(xiàn)了面向?qū)ο蟮?strong>低耦合的設(shè)計(jì)理念;通過這樣實(shí)現(xiàn)的程序能更加符合人的操作理念,更加貼合現(xiàn)實(shí)環(huán)境;同時(shí),也能很好的避免因生產(chǎn)者與消費(fèi)者直接交互而導(dǎo)致的操作不安全的問題。

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

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

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

關(guān)于高耦合和低耦合的區(qū)別,電腦中主機(jī)中的集成顯卡和獨(dú)立顯卡也是一個(gè)非常好的例子。

  • 集成顯卡普遍都集成于CPU中,所以如果集成顯卡出現(xiàn)了問題需要更換,那么會連著CPU一塊更換,其維護(hù)成本與CPU其實(shí)是一樣的;

  • 獨(dú)立顯卡需要插在主板的顯卡接口上才能與計(jì)算機(jī)通信,其相對于整個(gè)計(jì)算機(jī)系統(tǒng)來說,是獨(dú)立的存在,即便出現(xiàn)問題需要更換,也只更換顯卡即可。

案例的代碼實(shí)現(xiàn)

接下來我們使用多線程技術(shù)實(shí)現(xiàn)該案例,案例代碼如下:

蔬菜基地對象,VegetableBase.java

// VegetableBase.java

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

    // 超市實(shí)例
    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);
            }
        }
    }
}

消費(fèi)者對象,Consumer.java

// Consumer.java

// 消費(fèi)者
public class Consumer implements Runnable {

    // 超市實(shí)例
    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;
    // 蔬菜數(shù)量
    private Integer num;

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

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

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

}

運(yùn)行案例,App.java

// 案例應(yīng)用入口
public class App {

    public static void main(String[] args) {
        // 創(chuàng)建超市實(shí)例
        Supermarket supermarket = new Supermarket();
        // 蔬菜基地線程啟動, 開始往超市輸送蔬菜
        new Thread(new VegetableBase(supermarket)).start();
        new Thread(new VegetableBase(supermarket)).start();
        // 消費(fèi)者線程啟動,消費(fèi)者開始購買蔬菜
        new Thread(new Consumer(supermarket)).start();
        new Thread(new Consumer(supermarket)).start();
    }

}

發(fā)現(xiàn)了問題

運(yùn)行該案例,打印出運(yùn)行結(jié)果,外表一片祥和,可還是被敏銳的發(fā)現(xiàn)了問題,問題如下所示:

案例運(yùn)行中發(fā)現(xiàn)的問題

在一片看似祥和的打印結(jié)果中,出現(xiàn)了一個(gè)很不祥和的特例,生產(chǎn)基地在輸送蔬菜時(shí),黃瓜的數(shù)量一直都是1300顆,青菜的數(shù)量一直是1400顆,但是在消費(fèi)者消費(fèi)時(shí)卻出現(xiàn)了蔬菜名稱是黃瓜的,但數(shù)量卻是青菜的數(shù)量的情況。

之所以出現(xiàn)這樣的問題,是因?yàn)樵诒景咐蚕淼馁Y源中,多個(gè)線程共同競爭資源時(shí)沒有使用同步操作,而是異步操作,今兒導(dǎo)致了資源分配紊亂的情況;需要注意的是,并不是因?yàn)槲覀冊诎咐惺褂?code>Thread.sleep();模擬網(wǎng)絡(luò)延遲才導(dǎo)致問題出現(xiàn),而是本來就存在問題,使用Thread.sleep();只是讓問題更加明顯。

案例問題的解決

在本案例中需要解決的問題有兩個(gè),分別如下:

  1. 問題一:蔬菜名稱和數(shù)量不匹配的問題。
  2. 問題二:需要保證超市無貨時(shí)生產(chǎn),超市有貨時(shí)才消費(fèi)。

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

所以,技術(shù)方案可以使用同步代碼塊/同步方法/Lock機(jī)制來保持操作的同步性。

針對問題二的解決方案:給超市一個(gè)有無貨的狀態(tài)標(biāo)志,

  • 超市無貨時(shí),蔬菜基地輸送蔬菜補(bǔ)貨,此時(shí)生產(chǎn)基地線程可操作;

  • 超市有貨時(shí),消費(fèi)者線程可操作;就是:保證生產(chǎn)基地 ——> 共享資源 ——> 消費(fèi)者這個(gè)整個(gè)流程的完整運(yùn)行。

技術(shù)方案:使用線程中的等待和喚醒機(jī)制。

同步操作,分為同步代碼塊同步方法兩種。詳情可查看我的另外一篇關(guān)于多線程的文章:「JAVA」Java 線程不安全分析,同步鎖和Lock機(jī)制,哪個(gè)解決方案更好

  1. 在同步代碼塊中的同步鎖必須選擇多個(gè)線程共同的資源對象,當(dāng)前生產(chǎn)者線程在生產(chǎn)數(shù)據(jù)的時(shí)候(先擁有同步鎖),其他線程就在鎖池中等待獲取鎖;當(dāng)生產(chǎn)者線程執(zhí)行完同步代碼塊的時(shí)候,就會釋放同步鎖,其他線程開始搶鎖的使用權(quán),搶到后就會擁有該同步鎖,執(zhí)行完成后釋放,其他線程再開始搶鎖的使用權(quán),依次往復(fù)執(zhí)行。
  2. 多個(gè)線程只有使用同一個(gè)對象(就好比案例中的共享資源對象)的時(shí)候,多線程之間才有互斥效果,我們把這個(gè)用來做互斥的對象稱之為同步監(jiān)聽對象,又稱同步監(jiān)聽器、互斥鎖、同步鎖,同步鎖是一個(gè)抽象概念,可以理解為在對象上標(biāo)記了一把鎖。
  3. 同步鎖對象可以選擇任意類型的對象即可,只需要保證多個(gè)線程使用的是相同鎖對象即可。在任何時(shí)候,最多只能運(yùn)行一個(gè)線程擁有同步鎖。因?yàn)橹挥型奖O(jiān)聽鎖對象才能調(diào)用waitnotify方法,waitnotify方法存在于Object類中。

線程通信之 wait和notify方法

java.lang.Object中提供了用于操作線程通信的方法,詳情如下:

  • wait()執(zhí)行該方法的線程對象會釋放同步鎖,然后JVM把該線程存放到等待池中,等待著其他線程來喚醒該線程;
  • notify()執(zhí)行該方法的線程會喚醒在等待池中處于等待狀態(tài)的的任意一個(gè)線程,把線程轉(zhuǎn)到同步鎖池中等待;
  • notifyAll()執(zhí)行該方法的線程會喚醒在等待池中處于等待狀態(tài)的所有的線程,把這些線程轉(zhuǎn)到同步鎖池中等待;

注意:上述方法只能被同步監(jiān)聽鎖對象來調(diào)用,否則發(fā)生 IllegalMonitorStateException

wait和notify方法應(yīng)用實(shí)例

假設(shè)A線程B線程共同操作一個(gè)X對象(同步鎖),A、B線程可以通過X對象waitnotify方法來進(jìn)行通信,流程如下:

  1. 當(dāng)A線程執(zhí)行X對象的同步方法時(shí),A線程持有X對象的鎖,B線程沒有執(zhí)行機(jī)會,此時(shí)的B線程會在X對象的鎖池中等待;
  2. 當(dāng)A線程在同步方法中執(zhí)行X.wait()方法時(shí),A線程會釋放X對象的同步鎖,然后進(jìn)入X對象的等待池中;
  3. 接著,在X對象的鎖池中等待鎖的B線程獲取X對象的鎖,執(zhí)行X的另一個(gè)同步方法;
  4. 當(dāng)B線程在同步方法中執(zhí)行X.notify()方法時(shí),JVM會把A線程X對象的等待池中轉(zhuǎn)到X對象的同步鎖池中,等待獲取鎖的使用權(quán);
  5. 當(dāng)B線程執(zhí)行完同步方法后,會釋放擁有的鎖,然后A線程獲得鎖,繼續(xù)執(zhí)行同步方法;

基于上述機(jī)制,我們就可以使用同步操作 + wait和notify方法來解決案例中的問題了,重新來實(shí)現(xiàn)共享資源——超市對象:

// 超市
public class Supermarket {

    // 蔬菜名稱
    private String name;
    // 蔬菜數(shù)量
    private Integer num;
    // 超市是否為空
    private Boolean isEmpty = true;

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

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

        }   
    }
}

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

由于waitnotify方法,只能被同步監(jiān)聽鎖對象來調(diào)用,否則發(fā)生
IllegalMonitorStateException。從Java 5開始,提供了Lock機(jī)制,同時(shí)還有處理Lock機(jī)制的通信控制的Condition接口。Lock機(jī)制沒有同步鎖的概念,也就沒有自動獲取鎖和自動釋放鎖的這樣的操作了。

因?yàn)闆]有同步鎖,所以Lock機(jī)制中的線程通信就不能調(diào)用waitnotify方法了;同樣的,Java 5 中也提供了解決方案,因此從Java 5開始,可以:

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

Lock和Condition接口的性能也比同步操作要高很多,所以這種方式也是我們推薦使用的方式。

我們可以使用Lock機(jī)制和Condition接口 方法來解決案例中的問題,重新來實(shí)現(xiàn)的共享資源——超市對象,代碼如下:

// 超市
public class Supermarket {

    // 蔬菜名稱
    private String name;
    // 蔬菜數(shù)量
    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) {   // 超市有貨時(shí),不再輸送蔬菜,而是要等待消費(fèi)者獲取
                   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) { // 超市無貨時(shí),不再提供消費(fèi),而是要等待蔬菜基地輸送
                   condition.await();
            }
            // 為了讓效果更明顯,在這里模擬網(wǎng)絡(luò)延遲
            Thread.sleep(1000);
            System.out.println("蔬菜:" + this.name + ", " + this.num + "顆。");
            isEmpty = true;
            condition.signalAll();  
        } catch (Exception e) {
                
        }   finally {
                lock.unlock();
        }
    }
}

完結(jié),老夫雖不正經(jīng),但老夫一身的才華!關(guān)注我,獲取更多編程科技知識。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容