首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> 關於C

Java 多執行緒經典案例:火車站三窗口同步售票系統

     編輯:關於C

問題場景

火車站有 100 張票,三個窗口(執行緒)同時售票。需確保:

  • 每張票只賣一次(無重賣、無漏賣)
  • 票賣完後所有窗口停止
  • 輸出每張票的賣出窗口與剩餘票數

核心程式碼

class PP implements Runnable {
    public int tickets = 100;           // 共享票數
    String str = new String("123");     // 同步鎖物件
    
    public void run() {
        while (true) {
            synchronized (str) {        // 同步區塊
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() 
                        + "賣出去的票數是:" + tickets);
                    tickets--;
                } else {
                    break;              // 票賣完,跳出迴圈結束執行緒
                }
            }
        }
    }
}

public class Threade_14 {
    public static void main(String[] args) {
        PP pp = new PP();               // 單一任務實例,共享 tickets
        Thread tt1 = new Thread(pp);    // 窗口 1
        Thread tt2 = new Thread(pp);    // 窗口 2
        Thread tt3 = new Thread(pp);    // 窗口 3
        tt1.start();
        tt2.start();
        tt3.start();
    }
}

關鍵設計解析

1. 共享資料封裝在 Runnable 實例中

  • tickets 為 PP 實例變數
  • 三個 Thread 共用同一 PP 實例 pp
  • 自然實現資料共享,無需 static 或外部容器

2. 同步鎖物件選擇

String str = new String("123");  // 專用鎖物件
synchronized (str) { ... }
  • 使用獨立 String 物件作為鎖,避免鎖住 this 或類字面常數造成外部干擾
  • synchronized(this) 也可行,但若外部程式碼也鎖 pp 實例會擴大競爭範圍
  • ❌ synchronized("123") 錯誤:字串常數池可能被其他類別共用,導致非預期阻塞

3. 同步區塊粒度

synchronized (str) {
    if (tickets > 0) {  // 檢查與遞減在同一原子操作中
        print...
        tickets--;
    } else break;
}
  • 將「檢查條件」與「修改共享變數」放在同一關鍵區段
  • 避免「檢查通過 → 切換執行緒 → 另一執行緒也通過檢查 → 重複賣同一張票」的競態條件

執行流程模擬

Thread-0賣出去的票數是:100
Thread-1賣出去的票數是:99
Thread-2賣出去的票數是:98
Thread-0賣出去的票數是:97
...
Thread-1賣出去的票數是:1
Thread-2賣出去的票數是:0  ← 最後一張票
// 所有執行緒檢查 tickets<=0 後 break 結束

常見錯誤與修正

錯誤寫法問題修正
synchronized(tickets)基本型別無法加鎖用物件鎖或 Integer 包裝類(但自動拆箱有風險)
if(tickets>0) synchronized {...}檢查在鎖外,競態條件檢查移入 synchronized 區塊內
用 static tickets + synchronized(PP.class)可行但耦合度高實例變數 + 實例鎖更符合 OO 封裝
無 break,賣完繼續跑輸出負數票數else break 終止執行緒

現代 Java 改良版(Java 5+)

class TicketSeller implements Runnable {
    private final AtomicInteger tickets = new AtomicInteger(100);
    
    public void run() {
        while (true) {
            int current = tickets.decrementAndGet();  // 原子遞減並取得舊值
            if (current >= 0) {
                System.out.println(Thread.currentThread().getName() 
                    + "賣出票號:" + (current + 1) + " 剩餘:" + current);
            } else {
                tickets.incrementAndGet();  // 還原最後一次遞減
                break;
            }
        }
    }
}
  • AtomicInteger:無鎖原子操作,效能優於 synchronized
  • decrementAndGet():原子性「遞減並回傳新值」
  • 無需顯式鎖物件,程式碼更簡潔安全

進階考量

  • 公平性:預設 synchronized 非公平,可用 ReentrantLock(true) 實現公平鎖
  • 效能:高競爭下 AtomicInteger 或 LongAdder(Java 8)優於鎖
  • 擴展性:票務系統實務需考慮分散式一致性、資料庫交易、補償機制

這個經典案例完美展示了「共享可變狀態」下的同步核心原則:封裝共享資料、最小化關鍵區段、原子性檢查與修改。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved