Java多執行緒——生產者消費者示例

SachinLea發表於2019-03-04

1. 等待/通知機制

  等待/通知機制在我們生活中很常見,例如,餐廳服務員和廚師之間,只有廚師做好菜之後,通知服務員,服務員才能上菜;而在未做好菜之前,服務員只能等待廚師做菜。除了這個例子外,等待/通知機制中,最典型的就是生產者和消費者模型,下邊我們用程式碼實現該模型。

2. 單一生產者和消費者

  Java中等待/通知,通常使用Object類中的wait()方法阻塞執行緒,執行緒進入等待吃,notify()/notifyAll()方法喚醒執行緒。其中notify()notifyAll()方法的區別在於,notify()方法,值喚醒等待池執行緒中的一個執行緒,而notifyAll()喚醒所有等待池中的執行緒。下邊我們使用程式碼,實現等待/通知中的典型例子,生產者和消費者模型。
  以生產汽車為例,建立一個汽車工廠類,其中有兩個一個生產汽車和一個銷售汽車的方法:

@Slf4j
public class CarFactory {

    private int num;

    private Object obj;

    public CarFactory(Object obj) {
        this.obj = obj;
    }

    public void createCar() {
        synchronized (obj) {
            try {
                while (num == 10) {
                    log.info("當前數量={},暫停生產", num);
                    obj.wait();
                }
                num++;
                log.info("生產者:{}, 生產了一輛汽車,當前總量:{}", Thread.currentThread().getName(), num);
                obj.notify();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void consumerCar() {
        synchronized (obj) {
            try {
                while (num == 1) {
                    log.info("當前數量={},暫停銷售", num);
                    obj.wait();
                }
                num--;
                log.info("消費者:{}, 購買了了一輛汽車,當前總量:{}", Thread.currentThread().getName(), num);
                obj.notify();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
複製程式碼

建立兩個執行緒,分別指代生產者和消費者:

@AllArgsConstructor
public class Producer implements Runnable {

    private CarFactory carFactory;

    @Override
    public void run() {
        while (true) {
            carFactory.createCar();
        }
    }
}

@AllArgsConstructor
public class Consumer implements Runnable {

    private CarFactory carFactory;

    @Override
    public void run() {
        while (true) {
            carFactory.consumerCar();
        }
    }
}
複製程式碼

測試方法:

public static void main(String[] args) throws Exception {
    CarFactory carFactory = new CarFactory(new Object());
    Producer producer = new Producer(carFactory);
    Consumer consumer = new Consumer(carFactory);
    new Thread(producer, "生產者A").start();
    new Thread(consumer, "消費者A").start();
}
複製程式碼

  上邊建立了一個生產者和一個消費者,程式執行的過程大致如下:

  1. 生產者獲得鎖之後,開始生產產品;
  2. 生產者生產的數量達到10之後,呼叫wait()方法,阻塞生產者執行緒,生產者釋放鎖;
  3. 生產者釋放鎖之後,消費者獲得鎖,消費一個後,呼叫notify()方法,喚醒一個等待池中的執行緒,而此時等待池中只有一個生產者,因此喚醒生產者,等待消費者釋放鎖;
  4. 消費者消費的只剩一個之後,呼叫wait()方法,阻塞消費者,釋放鎖;
  5. 生產者再次得到鎖,然後再次從第1步開始執行。

  需要注意的地方:判斷執行緒是否該阻塞時,需要使用while而不是if;因為,執行緒喚醒之後,會繼續從上一次停止的地方開始執行;如果使用if,下一次喚醒之後,不會再次判斷,會繼續執行if後邊的程式碼,這裡就是會繼續生產產品,造成誤差,單個生產者消費者時,可能不明顯,多個生產者消費者時會更加明顯的體現。

以上測試程式碼,執行結果:

19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生產者:生產者A, 生產了一輛汽車,當前總量:2
19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生產者:生產者A, 生產了一輛汽車,當前總量:3
19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生產者:生產者A, 生產了一輛汽車,當前總量:4
19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生產者:生產者A, 生產了一輛汽車,當前總量:5
19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生產者:生產者A, 生產了一輛汽車,當前總量:6
19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生產者:生產者A, 生產了一輛汽車,當前總量:7
19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生產者:生產者A, 生產了一輛汽車,當前總量:8
19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生產者:生產者A, 生產了一輛汽車,當前總量:9
19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 生產者:生產者A, 生產了一輛汽車,當前總量:10
19:24:33.673 [生產者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 當前數量=10,暫停生產
19:24:33.673 [消費者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消費者:消費者A, 購買了了一輛汽車,當前總量:9
19:24:33.673 [消費者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消費者:消費者A, 購買了了一輛汽車,當前總量:8
19:24:33.673 [消費者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消費者:消費者A, 購買了了一輛汽車,當前總量:7
19:24:33.673 [消費者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消費者:消費者A, 購買了了一輛汽車,當前總量:6
19:24:33.673 [消費者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消費者:消費者A, 購買了了一輛汽車,當前總量:5
19:24:33.673 [消費者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消費者:消費者A, 購買了了一輛汽車,當前總量:4
19:24:33.673 [消費者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消費者:消費者A, 購買了了一輛汽車,當前總量:3
19:24:33.673 [消費者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消費者:消費者A, 購買了了一輛汽車,當前總量:2
19:24:33.673 [消費者A] INFO com.sachin.threadlearn.waitAndNotify.CarFactory - 消費者:消費者A, 購買了了一輛汽車,當前總量:1
.........
複製程式碼

3. 多個生產者和消費者

  上邊展示了單個生產者消費者的情況,下邊看一下多個生產者消費者的情況;之前我們說過notify()方法,只會喚醒等待池中的一個執行緒,在多個生產者消費者的情況下,如果繼續使用notify()方法,每次就只能喚醒一個執行緒;
  更加嚴重的情況可能會出現“假死”,即,消費者喚醒了一個生產者,然後釋放鎖,生產者A開始生產,同時,呼叫notify()方法喚醒執行緒,如果喚醒了另一個生產者B;那麼,當達到阻塞條件,生產者A進入WAITING狀態,生產者B得到鎖,開始執行同步塊,但是剛執行就發現達到了阻塞條件也進入WAITING狀態;此時,還沒來得及喚醒執行緒;所有的執行緒都處於WAITING狀態,就是執行緒“假死”。
  因此,多個生產者消費者我們需要使用notifyAll()方法,喚醒所有的執行緒,其他地方可以不做修改;測試方法,多建立幾個生產者和消費者就可以了,這裡不再重複程式碼。
  但是,使用notifyAll()方法,喚醒了所有的等待池中的執行緒,但是,生產者生產過程中,如果即將達到阻塞狀態時,喚醒所有執行緒,喚醒的生產者還是會再次阻塞,浪費時間;如果我們只喚醒消費者就了,使用LockCondition可以實現我們的需求:

@Slf4j
public class CarFactory {

    private int num;

    private Lock lock = new ReentrantLock();

    private Condition producerCondition = lock.newCondition();

    private Condition consumerCondition = lock.newCondition();

    public void producerCar() {
        try {
            lock.lock();
            while (num == 10) {
                log.info("數量已達上限,停止生產");
                producerCondition.await();
            }
            num++;
            log.info("生產者:{}, 生產了一個汽車,總量為:{}", Thread.currentThread().getName(), num);
            consumerCondition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void consumerCar() {
        try {
            lock.lock();
            while (num == 1) {
                log.info("沒車了,停止銷售");
                consumerCondition.await();
            }
            num--;
            log.info("消費者:{}, 購買了一個汽車,總量為:{}", Thread.currentThread().getName(), num);
            producerCondition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
複製程式碼

  上邊程式碼,建立了兩個Condition物件,分別用來阻塞和喚醒生產者消費者;生產方法中,使用producerCondition物件,阻塞生產者,但是使用consumerCondition物件的singAll()方法,來喚醒執行緒,因為消費者使用了也是該物件進行阻塞,因此,此時喚醒的都是消費者;對於消費者也是一樣。
測試方法:

public static void main(String[] args) {
    CarFactory carFactory = new CarFactory();
    ExecutorService service = Executors.newCachedThreadPool();
    Producer producer = new Producer(carFactory);
    Consumer consumer = new Consumer(carFactory);
    for (int i = 0; i < 3; i++) {
        service.execute(producer);
        service.execute(consumer);
    }
    service.shutdown();
}
複製程式碼

相關文章