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

Java 多線程入門實戰:模擬售票系統演示 synchronized 同步機制

     編輯:Java 相關知識

前言

多線程併發控制是 Java 核心難點。經典的「多窗口售票」問題完美演示了臨界區競爭與 synchronized 互斥鎖的基本用法。本文通過完整可運行代碼,展示 10 個線程搶賣 100 張票,保證每張票只賣一次、編號不重不漏。

問題描述

  • 共享資源:100 張票(編號 100 遞減至 1)。
  • 10 個售票點(線程),每 100ms 賣一張。
  • 要求:輸出「第 X 售票點賣出第 Y 張票」,票號連續、無重複、無跳號。

代碼實現

1. 資源類 —— 封裝共享數據與同步操作


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; // 標記售罄,線程將退出循環
        }
    }
}

2. 售票點任務類 —— 實現 Runnable


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(); }
        }
    }
}

3. 主程序啟動 10 個線程


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,無重複、無遺漏,證明同步有效。

核心知識點深度解析

1. synchronized 作用於實例方法

  • 鎖對象 = this (Resource 實例)。
  • 所有線程共享同一個 Resource 實例 → 共用同一把鎖 → 互斥進入 sellTicket。

2. 為什麼不用 static 方法?

若改為 static synchronized,鎖為 Resource.class,同樣有效。但實例方法更符合 OO 封裝,支持多資源實例隔離。

3. 可見性問題:soldOut 需 volatile

代碼中 soldOut 由同步方法寫入,非同步讀取(while 循環)。JMM 不保證寫入對其他線程立即可見。生產環境必須加 volatile:

volatile boolean soldOut = false;

4. 睡眠在同步塊外

Thread.sleep(100) 放在 sellTicket() 外部,避免持有鎖睡眠降低併發度。這是「縮小同步範圍」原則的體現。

5. 原子性 vs 復合操作

if (ticketNum > 0) { print; ticketNum--; } 是「檢查-執行」復合操作,必須整體原子化。若拆分為 getTicketNum() + decrease() 兩個同步方法,中間仍有競態窗口。

進階對比:Lock / AtomicInteger / 並發集合

方案代碼示例優缺點
synchronized如上簡單、JVM 優化好(偏向鎖/輕量級鎖/重量級鎖升級)、條件等待需配合 wait/notify
ReentrantLocklock.lock(); try{...} finally{lock.unlock();}可中斷、公平鎖、多 Condition、手動釋放靈活
AtomicIntegerAtomicInteger tickets = new AtomicInteger(100);
int t = tickets.decrementAndGet();
無鎖 CAS、適合簡單計數、無法原子化「檢查+打印+遞減」復合邏輯
ConcurrentLinkedQueueQueue<Integer> q = ...; q.poll();無鎖隊列、適合生產消費、票號預生成入隊

總結

  • synchronized 是入門首選,語義清晰、JVM 原生優化。
  • 鎖保護的共享可變狀態必須包含「檢查-執行」完整邏輯。
  • 標記變量跨線程讀寫需 volatile 保證可見性。
  • 耗時操作(I/O、睡眠)移出同步塊,提高吞吐率。

原文鏈接:OSChina 代碼片段

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