多線程售票案例
1. 前言
本節內容主要是使用 Java 的鎖機制對多線程售票案例進行實現。售票案例多數情況下主要關注多線程如何安全的減少庫存,也就是剩余的票數,當票數為 0 時,停止減少庫存。
本節內容除了關注車票庫存的減少,還會涉及到退票窗口,能夠更加貼切的模擬真實的場景。
本節內容需要學習者關注如下兩個重點:
- 掌握多線程的售票機制模型,在后續的工作中如果涉及到類似的場景,能夠第一時間了解場景的整體結構;
- 使用 Condition 和 Lock 實現售票機制,鞏固我們本章節內容所學習的新的鎖機制。
2. 售票機制模型
售票機制模型是源于現實生活中的售票場景,從開始的單窗口售票到多窗口售票,從開始的人工統計票數到后續的系統智能在線售票。多并發編程能夠實現這一售票場景,多窗口售票情況下保證線程的安全性和票數的正確性。
如上圖所示,有兩個售票窗口進行售票,有一個窗口處理退票,這既是現實生活中一個簡單的售票機制。
3. 售票機制實現
場景設計:
- 創建一個工廠類 TicketCenter,該類包含兩個方法,saleRollback 退票方法和 sale 售票方法;
- 定義一個車票總數等于 10 ,為了方便觀察結果,設置為 10。學習者也可自行選擇數量;
- 對于 saleRollback 方法,當發生退票時,通知售票窗口繼續售賣車票;
- 對 saleRollback 進行特別設置,每隔 5000 毫秒退回一張車票;
- 對于 sale 方法,只要有車票就進行售賣。為了更便于觀察結果,每賣出一張車票,sleep 2000 毫秒;
- 創建一個測試類,main 函數中創建 2 個售票窗口和 1 個退票窗口,運行程序進行結果觀察。
- 修改 saleRollback 退票時間,每隔 25 秒退回一張車票;
- 再次運行程序并觀察結果。
實現要求:本實驗要求使用 ReentrantLock 與 Condition 接口實現同步機制。
實例:
public class DemoTest {
public static void main(String[] args) {
TicketCenter ticketCenter = new TicketCenter();
new Thread(new saleRollback(ticketCenter),"退票窗口"). start();
new Thread(new Consumer(ticketCenter),"1號售票窗口"). start();
new Thread(new Consumer(ticketCenter),"2號售票窗口"). start();
}
}
class TicketCenter {
private int capacity = 10; // 根據需求:定義10漲車票
private Lock lock = new ReentrantLock(false);
private Condition saleLock = lock.newCondition();
// 根據需求:saleRollback 方法創建,為退票使用
public void saleRollback() {
try {
lock.lock();
capacity++;
System.out.println("線程("+Thread.currentThread().getName() + ")發生退票。" + "當前剩余票數"+capacity+"個");
saleLock.signalAll(); //發生退票,通知售票窗口進行售票
} finally {
lock.unlock();
}
}
// 根據需求:sale 方法創建
public void sale() {
try {
lock.lock();
while (capacity==0) { //沒有票的情況下,停止售票
try {
System.out.println("警告:線程("+Thread.currentThread().getName() + ")準備售票,但當前沒有剩余車票");
saleLock.await(); //剩余票數為 0 ,無法售賣,進入 wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
capacity-- ; //如果有票,則售賣 -1
System.out.println("線程("+Thread.currentThread().getName() + ")售出一張票。" + "當前剩余票數"+capacity+"個");
} finally {
lock.unlock();
}
}
}
class saleRollback implements Runnable {
private TicketCenter TicketCenter; //關聯工廠類,調用 saleRollback 方法
public saleRollback(TicketCenter TicketCenter) {
this.TicketCenter = TicketCenter;
}
public void run() {
while (true) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TicketCenter.saleRollback(); //根據需求 ,調用 TicketCenter 的 saleRollback 方法
}
}
}
class Consumer implements Runnable {
private TicketCenter TicketCenter;
public Consumer(TicketCenter TicketCenter) {
this.TicketCenter = TicketCenter;
}
public void run() {
while (true) {
TicketCenter.sale(); //調用sale 方法
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
結果驗證:
線程(1號售票窗口)售出一張票。當前剩余票數9個
線程(2號售票窗口)售出一張票。當前剩余票數8個
線程(2號售票窗口)售出一張票。當前剩余票數7個
線程(1號售票窗口)售出一張票。當前剩余票數6個
線程(1號售票窗口)售出一張票。當前剩余票數5個
線程(2號售票窗口)售出一張票。當前剩余票數4個
線程(退票窗口)發生退票。當前剩余票數5個
線程(1號售票窗口)售出一張票。當前剩余票數4個
線程(2號售票窗口)售出一張票。當前剩余票數3個
線程(2號售票窗口)售出一張票。當前剩余票數2個
線程(1號售票窗口)售出一張票。當前剩余票數1個
線程(退票窗口)發生退票。當前剩余票數2個
線程(1號售票窗口)售出一張票。當前剩余票數1個
線程(2號售票窗口)售出一張票。當前剩余票數0個
警告:線程(1號售票窗口)準備售票,但當前沒有剩余車票
警告:線程(2號售票窗口)準備售票,但當前沒有剩余車票
線程(退票窗口)發生退票。當前剩余票數1個
線程(1號售票窗口)售出一張票。當前剩余票數0個
警告:線程(2號售票窗口)準備售票,但當前沒有剩余車票
警告:線程(1號售票窗口)準備售票,但當前沒有剩余車票
結果分析:從結果來看,我們正確的完成了售票和退票的機制,并且使用了 ReentrantLock 與 Condition 接口。
代碼片段分析 1:看售票方法代碼。
public void sale() {
try {
lock.lock();
while (capacity==0) { //沒有票的情況下,停止售票
try {
System.out.println("警告:線程("+Thread.currentThread().getName() + ")準備售票,但當前沒有剩余車票");
saleLock.await(); //剩余票數為 0 ,無法售賣,進入 wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
capacity-- ; //如果有票,則售賣 -1
System.out.println("線程("+Thread.currentThread().getName() + ")售出一張票。" + "當前剩余票數"+capacity+"個");
} finally {
lock.unlock();
}
}
主要來看方法中僅僅使用了 await 方法,因為退票是場景觸發的,售票窗口無需喚醒退票窗口,因為真實的場景下,可能沒有退票的發生,所以無需喚醒。這與生產者與消費者模式存在著比較明顯的區別。
代碼片段分析 2:看退票方法代碼。
public void saleRollback() {
try {
lock.lock();
capacity++;
System.out.println("線程("+Thread.currentThread().getName() + ")發生退票。" + "當前剩余票數"+capacity+"個");
saleLock.signalAll(); //發生退票,通知售票窗口進行售票
} finally {
lock.unlock();
}
}
退票方法只有 signalAll 方法,通知售票窗口進行售票,無需調用 await 方法,因為只要有退票的發生,就能夠繼續售票,沒有庫存上限的定義,這也是與生產者與消費者模式的一個主要區別。
總結:售票機制與生產者 - 消費者模式存在著細微的區別,需要學習者通過代碼的實現慢慢體會。由于售票方法只需要進入 await 狀態,退票方法需要喚醒售票的 await 狀態,因此只需要創建一個售票窗口的 Condition 對象。
4. 小結
本節內容主要對售票機制模型進行了講解,核心內容為售票機制的實現。實現的過程使用 ReentrantLock 與 Condition 接口實現同步機制,也是本節課程的重點知識。
至此,并發編程原理課程就結束了,從基本的多線程的創建,synchronized 關鍵字的使用,再到鎖的應用,貫穿了并發編程原理的全部知識,學習完本套課程,可以進一步學習并發編程包的課程。