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

Java 多線程實戰:線程同步機制詳解與售票案例

     編輯:Java 綜合教學

前言

多線程環境下,多個線程同時訪問共享資源會導致數據不一致。本文通過經典「電影院售票」案例,深入講解線程安全問題成因,並詳細對比三種同步解決方案:同步代碼塊、同步方法、Lock 顯式鎖。

一、問題引入:售票系統的線程安全隱患

1. 業務場景

電影院 100 張票,3 個窗口並發售票。初始版本無同步控制。

2. 兩種線程創建方式對比

方式一:繼承 Thread 類

public class SellTicket extends Thread {
    private static int tickets = 100;  // static 保證共享
    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(getName() + " 正在出售第 " + (tickets--) + " 張票");
            }
        }
    }
}

方式二:實現 Runnable 接口(推薦)

public class SellTicket implements Runnable {
    private int tickets = 100;  // 同一資源對象天然共享
    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 張票");
            }
        }
    }
}

核心差異:Runnable 將任務與線程分離,一個資源對象可被多線程共享,更符合面向對象設計。

3. 並發問題複現:引入網絡延遲

// 在售票邏輯中加入延遲模擬真實場景
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(... + (tickets--) + ...);

出現的錯誤現象:

  • 同一張票被多次賣出(如第 1 張票被窗口 1、2、3 同時賣出)
  • 出現負數票數(-1、-2...)

根因分析:tickets-- 非原子操作(讀取-修改-寫入),多線程交替執行導致數據競態。

二、線程安全問題判斷標準

滿足以下三個條件即存在線程安全隱患:

  1. 多線程環境
  2. 存在共享數據
  3. 多條語句操作共享數據(非原子操作)

三、三種同步解決方案深度對比

方案 1:同步代碼塊

private Object obj = new Object();  // 任意對象作為鎖
@Override
public void run() {
    while (true) {
        synchronized (obj) {  // 同步代碼塊
            if (tickets > 0) {
                try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 張票");
            }
        }
    }
}

關鍵點:多線程必須共用同一把鎖對象。此處 obj 為成員變量,Runnable 實例共享,滿足條件。

方案 2:同步方法

① 實例方法同步

private synchronized void sellTicket() {  // 鎖對象隱式為 this
    if (tickets > 0) {
        try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 張票");
    }
}

② 靜態方法同步

private static synchronized void sellTicket() {  // 鎖對象為 類.class
    // ...
}

鎖對象規律:

方法類型隱式鎖對象
實例方法this(當前實例)
靜態方法當前類的 Class 對象(如 SellTicket.class)

方案 3:Lock 顯式鎖(JDK 5+ 推薦)

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

private Lock lock = new ReentrantLock();

@Override
public void run() {
    while (true) {
        try {
            lock.lock();  // 顯式加鎖
            if (tickets > 0) {
                try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 張票");
            }
        } finally {
            lock.unlock();  // 必須在 finally 中釋放,防止死鎖
        }
    }
}

優勢:鎖的獲取/釋放可見、可控,支持嘗試獲取鎖、可中斷鎖、公平鎖等高級特性。

四、同步的優缺點與選型建議

方案優點缺點適用場景
同步代碼塊粒度精細,鎖對象靈活代碼冗長,易忘記加鎖只需同步部分邏輯
同步方法代碼簡潔鎖粒度較粗(整個方法),並發度低整個方法均需同步
Lock 顯式鎖功能強大,性能優於 synchronized(高併發下)需手動 unlock,忘記釋放導致死鎖複雜同步邏輯、需公平鎖/可中斷鎖

五、總結

線程同步的核心是將多條操作共享數據的代碼「鎖起來」,任意時刻只允許一個線程執行。實際開發中:

  • 優先使用 Lock(如 ReentrantLock)替代 synchronized
  • 儘量縮小同步塊範圍,提高並發度
  • 嚴格遵循「加鎖在 try,解鎖在 finally」原則
  • 避免在同步塊中調用耗時操作(I/O、sleep 等)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved