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();
}
複製程式碼
上邊建立了一個生產者和一個消費者,程式執行的過程大致如下:
- 生產者獲得鎖之後,開始生產產品;
- 生產者生產的數量達到10之後,呼叫
wait()
方法,阻塞生產者執行緒,生產者釋放鎖; - 生產者釋放鎖之後,消費者獲得鎖,消費一個後,呼叫
notify()
方法,喚醒一個等待池中的執行緒,而此時等待池中只有一個生產者,因此喚醒生產者,等待消費者釋放鎖; - 消費者消費的只剩一個之後,呼叫
wait()
方法,阻塞消費者,釋放鎖; - 生產者再次得到鎖,然後再次從第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()
方法,喚醒了所有的等待池中的執行緒,但是,生產者生產過程中,如果即將達到阻塞狀態時,喚醒所有執行緒,喚醒的生產者還是會再次阻塞,浪費時間;如果我們只喚醒消費者就了,使用Lock
和Condition
可以實現我們的需求:
@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();
}
複製程式碼