多線程環境下,多個線程同時訪問共享資源會導致數據不一致。本文通過經典「電影院售票」案例,深入講解線程安全問題成因,並詳細對比三種同步解決方案:同步代碼塊、同步方法、Lock 顯式鎖。
電影院 100 張票,3 個窗口並發售票。初始版本無同步控制。
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--) + " 張票");
}
}
}
}
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 將任務與線程分離,一個資源對象可被多線程共享,更符合面向對象設計。
// 在售票邏輯中加入延遲模擬真實場景
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(... + (tickets--) + ...);
出現的錯誤現象:
根因分析:tickets-- 非原子操作(讀取-修改-寫入),多線程交替執行導致數據競態。
滿足以下三個條件即存在線程安全隱患:
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 實例共享,滿足條件。
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) |
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