多線程併發控制是 Java 核心難點。經典的「多窗口售票」問題完美演示了臨界區競爭與 synchronized 互斥鎖的基本用法。本文通過完整可運行代碼,展示 10 個線程搶賣 100 張票,保證每張票只賣一次、編號不重不漏。
class Resource {
int ticketNum = 100; // 共享票數
boolean soldOut = false;
// synchronized 修飾實例方法 → 鎖為 this (Resource 實例)
public synchronized void sellTicket(Seller seller) {
if (ticketNum > 0) {
System.out.println("第" + seller.id + "售票點賣出了第" + ticketNum + "張票……");
ticketNum--;
} else {
soldOut = true; // 標記售罄,線程將退出循環
}
}
}
class Seller implements Runnable {
int id; // 售票點編號 0-9
Resource resource; // 共享資源引用
public Seller(int id, Resource resource) {
this.id = id;
this.resource = resource;
}
@Override
public void run() {
while (!resource.soldOut) { // 讀取 volatile 建議加 volatile
resource.sellTicket(this); // 進入同步方法
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
public class TicketDemo {
public static void main(String[] args) {
Resource resource = new Resource(100);
for (int i = 0; i < 10; i++) {
new Thread(new Seller(i, resource)).start();
}
}
}

輸出示例:
第0售票點賣出了第100張票…… 第2售票點賣出了第99張票…… 第5售票點賣出了第98張票…… ... 第3售票點賣出了第1張票……
票號從 100 遞減至 1,無重複、無遺漏,證明同步有效。
this (Resource 實例)。sellTicket。若改為 static synchronized,鎖為 Resource.class,同樣有效。但實例方法更符合 OO 封裝,支持多資源實例隔離。
代碼中 soldOut 由同步方法寫入,非同步讀取(while 循環)。JMM 不保證寫入對其他線程立即可見。生產環境必須加 volatile:
volatile boolean soldOut = false;
Thread.sleep(100) 放在 sellTicket() 外部,避免持有鎖睡眠降低併發度。這是「縮小同步範圍」原則的體現。
if (ticketNum > 0) { print; ticketNum--; } 是「檢查-執行」復合操作,必須整體原子化。若拆分為 getTicketNum() + decrease() 兩個同步方法,中間仍有競態窗口。
| 方案 | 代碼示例 | 優缺點 |
|---|---|---|
| synchronized | 如上 | 簡單、JVM 優化好(偏向鎖/輕量級鎖/重量級鎖升級)、條件等待需配合 wait/notify |
| ReentrantLock | lock.lock(); try{...} finally{lock.unlock();} | 可中斷、公平鎖、多 Condition、手動釋放靈活 |
| AtomicInteger | AtomicInteger tickets = new AtomicInteger(100); int t = tickets.decrementAndGet(); | 無鎖 CAS、適合簡單計數、無法原子化「檢查+打印+遞減」復合邏輯 |
| ConcurrentLinkedQueue | Queue<Integer> q = ...; q.poll(); | 無鎖隊列、適合生產消費、票號預生成入隊 |
synchronized 是入門首選,語義清晰、JVM 原生優化。volatile 保證可見性。原文鏈接:OSChina 代碼片段