CodeGym /課程 /JAVA 25 SELF /同步化常見錯誤解析

同步化常見錯誤解析

JAVA 25 SELF
等級 52 , 課堂 4
開放

1. 遺漏的 unlock/release:粗心者的陷阱

在使用現代同步化工具(例如 ReentrantLockSemaphore)時,最陰險的錯誤之一,就是忘記呼叫 unlock()release()。如果你不釋放鎖,其他執行緒將會一直等待它被釋放……永遠。程式會掛住,而你會盯著螢幕,苦思為何什麼都沒發生。

ReentrantLock 為例:

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

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        // 糟了!忘了呼叫 unlock()——大家都會卡住!
        count++;
    }
}

看起來毫無問題,但若從不同執行緒多次呼叫 increment(),在第一次呼叫之後,其餘執行緒會無限期地等待鎖被釋放。

為避免此情況,請使用結構 try-finally

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

如此一來,即使在方法中途發生例外,鎖也會被保證釋放。

這就像有人占了廁所(從裡面反鎖),然後忘了開門就從窗戶離開。其他人會一直等著那個人出來……別這麼做!

2. 在錯誤的物件上同步化:「哎呀,鎖掛錯地方了!」

在 Java 中,關鍵字 synchronized 可以鎖定對某個物件的存取。但如果你選錯了用來鎖定的物件,實際的同步化行為就不會如你所願。

錯誤 #1:針對區域變數進行同步化

public void doSomething() {
    Object lock = new Object();
    synchronized (lock) {
        // 每次都是新物件——根本沒有同步化!
        // 執行緒彼此不會等待。
        // 臨界區沒有受到保護!
    }
}

此處每個執行緒都會建立自己的 lock 物件。結果根本不會產生真正的鎖定——執行緒會同時進入臨界區。

正確做法:

private final Object lock = new Object();

public void doSomething() {
    synchronized (lock) {
        // 現在所有執行緒都使用同一個 lock 物件
        // 並且真的會彼此等待。
    }
}

錯誤 #2:在字串常值上同步化

public void doSomething() {
    synchronized ("lock") {
        // 字串常值會被內駐(intern):程式的不同部分可能
        // 不小心都在同一個字串上同步化!
    }
}

結論:
只在私有、專門為此目的建立且不會在其他地方使用的物件上進行同步化。

3. 雙重鎖定(deadlock):「你等我—我等你,雙雙卡住」

Deadlock(互相封鎖)是經典問題。兩個(或更多)執行緒先後取得不同的鎖,然後彼此等待,直到程式卡死。

範例:

public class DeadlockExample {
    private final Object lockA = new Object();
    private final Object lockB = new Object();

    public void method1() {
        synchronized (lockA) {
            // 為了示範更明顯,稍微等待一下
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockB) {
                // ...
            }
        }
    }

    public void method2() {
        synchronized (lockB) {
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            synchronized (lockA) {
                // ...
            }
        }
    }
}

如果一個執行緒呼叫 method1(),而另一個呼叫 method2(),第一個執行緒會取得 lockA 並等待 lockB,第二個則相反。結果兩者會無限期地互相等待。

如何避免?

  • 在所有執行緒中,以相同的順序取得鎖。
  • 盡量減少同時持有的鎖數量。
  • 當程式卡住時,使用診斷工具(例如 jstack)。

類比:
這就像兩個人在狹窄的走廊相遇,彼此都決定要讓路,但前提是對方要先讓。結果兩人就僵在那裡,等著看誰先讓步。

4. 過度同步化:「寧可多鎖,毋寧少鎖?」 — 不一定!

有時開發者因害怕錯誤,對所有東西都加鎖。結果效能下降,卻毫無助益。

範例:

public synchronized void add(int value) {
    // 這裡只有一行,根本不需要同步化!
    System.out.println("已加入: " + value);
}

在這種情況下不需要同步化:透過 System.out.println 的輸出本身就已具備執行緒安全,而方法本身也沒有操作共用資源。

什麼時候會很嚴重?
如果你對經常被呼叫且不需要保護的方法進行同步化,會大幅降低程式效能。執行緒會排隊等鎖,明明可以並行工作。

Best practice:
只同步化真正需要的部分。臨界區應該越小越好。

5. 錯誤使用 volatile:「有可見性,沒有原子性!」

在 Java 中,修飾詞 volatile 保證變更對所有執行緒可見,但它保證操作的原子性。

錯誤:

private volatile int counter = 0;

public void increment() {
    counter++; // 非原子操作!
}

counter++ 由讀取、遞增、再寫回組成。如果兩個執行緒同時執行這段程式碼,最終數值可能小於預期。

正確做法:
對需要原子性的操作,使用 synchronizedAtomicInteger 或其他具備執行緒安全的類別。

import java.util.concurrent.atomic.AtomicInteger;

private final AtomicInteger counter = new AtomicInteger();

public void increment() {
    counter.incrementAndGet();
}

何時該用 volatile?
用於簡單的旗標(例如「結束工作」),當不需要原子性時。

1
問卷/小測驗
執行緒同步化,等級 52,課堂 4
未開放
執行緒同步化
執行緒同步化
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION