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

Java 併發經典問題:多線程賣票為何出現重複與負數?

     編輯:程式設計綜合問答

代碼現象

class Ticket implements Runnable {
    private int num = 100;
    public void run() {
        while (true) {
            if (num > 0) {
                Thread.sleep(2);
                System.out.println(Thread.currentThread().getName() + " sale " + num--);
            } else break;
        }
    }
}

輸出中出現:同一張票(如 8)被賣 4 次,甚至出現負數票(-1, -2)。

根本原因:複合操作非原子 + 線程調度競態

  1. if (num > 0) —— 檢查
  2. System.out.println(...) —— 輸出(耗時,易被搶佔)
  3. num-- —— 遞減(讀取-修改-寫入,非原子)

三步組成「檢查-輸出-修改」複合操作,**非原子**,線程間可任意交錯:

線程A: if(8>0) → true
線程B: if(8>0) → true
線程C: if(8>0) → true
線程D: if(8>0) → true
線程A: print 8 → num=7
線程B: print 8 → num=6
線程C: print 8 → num=5
線程D: print 8 → num=4  // 8被賣4次
...
線程X: if(0>0) → false? 但已被搶佔執行print -- → -1

解決方案:同步機制保證原子性

方案一:synchronized 同步方法(推薦入門)

public synchronized void sale() {
    if (num > 0) {
        try { Thread.sleep(2); } catch (InterruptedException e) {}
        System.out.println(Thread.currentThread().getName() + " sale " + num--);
    }
}

// run() 中調用 sale()

方案二:synchronized 同步代碼塊(粒度更細)

public void run() {
    while (true) {
        synchronized (this) {  // this 指向共享的 Ticket 實例
            if (num > 0) {
                try { Thread.sleep(2); } catch (InterruptedException e) {}
                System.out.println(Thread.currentThread().getName() + " sale " + num--);
            } else break;
        }
    }
}

方案三:ReentrantLock 顯式鎖(功能更強)

private Lock lock = new ReentrantLock();
public void run() {
    while (true) {
        lock.lock();
        try {
            if (num > 0) {
                try { Thread.sleep(2); } catch (InterruptedException e) {}
                System.out.println(Thread.currentThread().getName() + " sale " + num--);
            } else break;
        } finally {
            lock.unlock();
        }
    }
}

方案四:AtomicInteger 無鎖 CAS(高性能)

private AtomicInteger num = new AtomicInteger(100);
public void run() {
    while (true) {
        int current = num.get();
        if (current > 0) {
            if (num.compareAndSet(current, current - 1)) {
                System.out.println(Thread.currentThread().getName() + " sale " + current);
            }
            // else 重試
        } else break;
    }
}

核心原則總結

  1. 複合操作必須原子化:檢查+修改、讀取+寫入等多步操作需加鎖或用原子類。
  2. 鎖保護的共享變量要一致:所有訪問 num 的代碼路徑必須持有同一把鎖。
  3. 睡眠/IO 不要在同步塊中:示例為教學簡化,實際應將 sleep/println 移出同步塊,縮小臨界區。

理解「重複賣票」本質是「檢查-執行」非原子,是 Java 併發編程入門的關鍵一課。

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