1. 程式人生 > 其它 >十、執行緒協作

十、執行緒協作

一、生產者消費者問題

1. 問題

  1. 假設倉庫中只能放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中的產品取走消費
  2. 如果倉庫中沒有產品,則生產者將產品放入倉庫。否則停止生產並等待,直到倉庫中的產品被消費者取走為止
  3. 如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止

2. 分析

生產者和消費者共享同一個資源,並且兩者之間相互依賴,互為條件

  • 對於生產者,沒有生產產品之前要通知消費者等待。而生產了產品之後又需要立即通知消費者
  • 對於消費者,在消費之後要通知生產者已經結束消費,需要生產新的產品
  • 在生產者消費者問題中,僅有synchronized是不夠的
    • synchronized
      可阻止併發更新同一個共享資源,實現了同步
    • synchronized不能用來實現不同執行緒之間的通訊

二、執行緒通訊的方法

Java 中提供了幾個方法解決執行緒之間的通訊問題

方法名 作用
wait() 表示執行緒一致等待,直到其他執行緒通知。與sleep()不同,會釋放鎖
wait(long timeout) 指定等待的毫秒數
notify() 喚醒一個處於等待狀態的執行緒
notifyAll() 喚醒同一個物件上所有呼叫wait()方法的執行緒,優先級別高的執行緒優先排程

注:以上均是 Object 類的方法,都只能在同步方法或者同步程式碼塊中使用,否則會丟擲IllegalMonitorStateExeception

異常

三、解決方式

1. 管程法

  • 生產者:負責生產資料的模組(可能是方法、物件、執行緒、程序)
  • 消費者:負責處理資料的模組(可能是方法、物件、執行緒、程序)
  • 緩衝區:消費者不能直接使用生產者的資料,他們之間有個“緩衝區”

生產者將生產好的資料放入緩衝區,消費者從緩衝區拿出資料

例:
public class TestPC {
    public static void main(String[] args) {
        SyncContainer container = new SyncContainer();
        new Producer(container).start();
        new Consumer(container).start();
    }
}

class Producer extends Thread {
    SyncContainer container;

    public Producer(SyncContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        // 生產
        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生產了" + i + "只雞");
        }
    }
}

class Consumer extends Thread {
    SyncContainer container;

    public Consumer(SyncContainer container) {
        this.container = container;
    }
    @Override
    public void run() {
        // 消費
        for (int i = 0; i < 100; i++) {
            System.out.println("消費了第" + container.pop().id + "只雞");
        }
    }
}

class Chicken {
    int id;
    public Chicken(int id) {
        this.id = id;
    }
}

class SyncContainer {
    // 需要一個容器大小
    Chicken[] chickens = new Chicken[10];
    // 容器計數器
    int count = 0;

    // 生產者放入產品
    public synchronized void push(Chicken c) {
        // 如果容器滿了,等待消費者消費
        if (count == chickens.length) {
            // 通知消費者,生產者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果未滿,需要丟入產品
        chickens[count] = c;
        count++;
        // 通知消費者可以消費了
        this.notifyAll();
    }
    // 消費者消費產品
    public synchronized Chicken pop() {
        // 判斷能否消費
        if (count == 0) {
            // 等待生產
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果可以消費
        count--;
        Chicken chicken = chickens[count];
        // 通知生產者生產
        this.notifyAll();
        return chicken;
    }
}

2. 訊號燈法

藉助標誌位判斷,若為真消費者等待,若為假生產者等待

例:
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

// 生產者——演員
class Player extends Thread {
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("快樂大本營");
            } else {
                this.tv.play("廣告");
            }
        }
    }
}

// 消費者——觀眾
class Watcher extends Thread {
    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

// 產品——節目
class TV {
    // 演員錄製時,觀眾等待 T
    // 觀眾觀看時,演員等待 F
    // 錄製的節目
    String show;
    // 標誌位
    boolean flag = true;

    // 演員錄製
    public synchronized void play(String show) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演員錄製了" + show);
        // 通知觀眾觀看
        this.notifyAll();
        this.show = show;
        this.flag = !this.flag;
    }

    // 觀眾觀看
    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("觀眾觀看了" + show);
        // 通知演員錄製
        this.notifyAll();
        this.flag = !this.flag;
    }
}

四、小結

  • 基於“鎖”的同步方式需要執行緒不斷地嘗試去獲得鎖,失敗了也需要繼續嘗試,這會很浪費資源,而等待/通知機制則能解決這一問題
  • 等待/通知機制使用的是同一個物件鎖,如果兩個執行緒使用的是不同物件鎖,則不能用該機制通訊

五、擴充套件

訊號量

  • 關鍵字volatile,能夠保證記憶體的可見性
  • 使用volatile修飾的變數,如果在一個執行緒裡值發生了改變,那麼在其它執行緒中都是立即可見的
  • 使用volatile修飾的變數需要進行原子操作