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

Java 線程同步基石(三):synchronized 與 volatile 核心原理與選型

     編輯:Java 相關知識

數據同步是多線程編程的核心難點。本文從「為什麼需要同步」切入,對比 synchronized 與 volatile 兩大基石機制的適用邊界。

競態條件成因

public class CharacterDisplayCanvas extends JComponent implements CharacterListener {
    private char[] tmpChar = new char[1];

    public synchronized void newCharacter(CharacterEvent ce) {
        tmpChar[0] = (char) ce.character;
        repaint(); // 觸發 paintComponent()
    }

    protected synchronized void paintComponent(Graphics gc) {
        // 讀取 tmpChar[0] 繪製
    }
}

race condition:newCharacter()(事件線程)與 paintComponent()(EDT 線程)並發訪問 tmpChar,且操作非原子(寫入 → 觸發重繪 vs 讀取 → 繪製)。

synchronized 原理:物件鎖語義

  • 線程進入同步方法/塊前必須獲取物件鎖
  • 鎖被佔用時,其他線程阻塞等待
  • 方法執行完畢(正常/異常)自動釋放鎖
  • 保證互斥訪問 + 內存可見性(鎖釋放前將寫入刷新至主存)

volatile:輕量級可見性保證

適用場景:單變量原子讀寫

private volatile boolean done = false; // 標記位

public void run() {
    while (!done) { /* ... */ } // 每次從主存讀取
}
public void setDone() { done = true; } // 寫入即時刷新主存

核心語義

  1. 可見性:讀取強制從主存,寫入強制刷新主存
  2. 原子性:單次讀/寫(含 long/double)為原子操作
  3. 禁止指令重排:JMM 屏障語義

為何不用 synchronized 保護 run()/setDone()?

// 錯誤示範:死鎖陷阱
public synchronized void run() { while (!done) { /* ... */ } }
public synchronized void setDone() { done = true; }

run() 持有鎖不釋放 → setDone() 永無法獲取鎖 → 死鎖。鎖範圍過大是死溫床。

volatile 邊界與誤區

非適用場景:複合操作

  • count++(讀-改-寫)非原子
  • 多變量不變式(如 if (a > 0) b = 1;)

數組陷阱

volatile int[] arr 僅保證引用可見,元素讀寫無保障。需用 AtomicIntegerArray 或鎖保護。

進階:同步塊替代 volatile 的「醜陋」模式

private int done;
public synchronized int getDone() { return done; }
public synchronized void setDone(int v) { done = v; }

通過同步 getter/setter 間接實現 volatile 效果,但引入鎖開銷,不如 volatile 優雅。

選型決策樹

  1. 單變量、單次讀/寫、無不變式依賴 → volatile
  2. 複合操作、多變量協作、需互斥 → synchronized / Lock
  3. 高併發計數器 → AtomicInteger / LongAdder

下文將延續計分板案例,實戰同步策略演進。

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