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

Effective Java 條目 67:避免過度同步——觀察者模式死鎖實戰、異常安全與同步區最小化原則

     編輯:Java 綜合教學

核心原則:同步區內「不調用外來方法」

為避免死鎖、數據競爭與異常安全問題,同步塊/方法內應僅執行簡單、確定性的操作(讀寫字段、集合操作),嚴禁調用可覆寫方法、回調、外部 API、耗時 I/O。

一、反面教材:觀察者模式同步死鎖

問題代碼(同步區內遍歷調用回調)

// 觀察者介面
public interface SetObserver<E> {
    void added(ObservableSet<E> set, E element);
}

// 被觀察集合:同步區內直接回調
public class ObservableSet<E> extends ForwardingSet<E> {
    private final List<SetObserver<E>> observers = new ArrayList<>();

    public void addObserver(SetObserver<E> o) {
        synchronized (observers) { observers.add(o); }
    }
    public boolean removeObserver(SetObserver<E> o) {
        synchronized (observers) { return observers.remove(o); }
    }

    // 危險:持有鎖遍歷並調用外來方法
    private void notifyElementAdded(E element) {
        synchronized (observers) {                // 持有鎖
            for (SetObserver<E> o : observers) {  // 遍歷
                o.added(this, element);           // 調用外來方法(可覆寫、不可控)
            }
        }
    }

    @Override public boolean add(E e) {
        boolean added = super.add(e);
        if (added) notifyElementAdded(e);
        return added;
    }
}

場景 1:回調中修改集合 → ConcurrentModificationException

set.addObserver(new SetObserver<Integer>() {
    public void added(ObservableSet<Integer> s, Integer e) {
        System.out.println(e);
        if (e == 23) s.removeObserver(this); // 回調中修改正在被遍歷的 observers 列表
    }
});
// 拋出 ConcurrentModificationException

場景 2:回調中嘗試獲取同一把鎖 → 死鎖

set.addObserver(new SetObserver<Integer>() {
    public void added(ObservableSet<Integer> s, Integer e) {
        if (e == 23) {
            ExecutorService exec = Executors.newSingleThreadExecutor();
            try {
                exec.submit(() -> s.removeObserver(this)).get(); // 子線程嘗試獲取 observers 鎖
            } catch (Exception ex) { throw new RuntimeException(ex); }
            finally { exec.shutdown(); }
        }
    }
});
// 主線程持有鎖調用回調 → 子線程阻塞等待鎖 → 主線程等待子線程結束(get()) → 死鎖

二、正確解法:快照模式(複製再遍歷)

// 安全:同步區內僅複製引用,釋放鎖後遍歷調用
private void notifyElementAddedSafe(E element) {
    List<SetObserver<E>> snapshot;
    synchronized (observers) {                    // 1. 僅複製,極短持鎖
        snapshot = new ArrayList<>(observers);
    }
    for (SetObserver<E> o : snapshot) {           // 2. 無鎖遍歷調用
        o.added(this, element);
    }
}

優勢:

  1. 回調中修改原 observers 不影響快照遍歷,無 CME。
  2. 回調中嘗試獲取 observers 鎖不會死鎖(主線程已釋放)。
  3. 異常拋出不污染同步區狀態,觀察者列表保持一致。

三、更優替代方案:CopyOnWriteArrayList(JUC 線程安全容器)

// 寫時複製,讀無鎖,天然適合「讀多寫少」觀察者列表
private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<>();

public void addObserver(SetObserver<E> o) { observers.add(o); }
public boolean removeObserver(SetObserver<E> o) { return observers.remove(o); }

private void notifyElementAdded(E element) {
    for (SetObserver<E> o : observers) { // 直接遍歷,無需同步
        o.added(this, element);
    }
}

適用場景:觀察者數量較少、添加/移除頻率低、遍歷頻率高。寫入會複製底層數組,大量寫入時效能下降。

四、異常安全:同步區內拋異常會破壞不變量

五、同步區最小化實戰清單

禁止在同步區內允許/推薦在同步區內
調用可覆寫方法(回調、接口方法、抽象方法)讀寫基本類型字段、volatile 字段
調用外部庫/第三方 API集合增刪改查(add/remove/get/put)
I/O 操作(網絡、磁盤、資料庫)簡單計算、邏輯判斷
長時間計算/睡眠複製集合引用/創建快照
嘗試獲取其他鎖(嵌套鎖風險)釋放鎖前的最後檢查

六、總結:Effective Java 條目 67 精華

  1. 同步區應盡可能小:只做「獲取鎖 → 讀寫共享狀態 → 釋放鎖」。
  2. 絕不在同步區內調用外來方法(Alienable Method):可覆寫方法、回調、Lambda、方法引用、外部庫。
  3. 需遍歷調用時,用快照模式或 CopyOnWriteArrayList。
  4. 異常邊界在同步區外,保證同步區要么完全成功,要么完全不執行(原子性)。
  5. 鎖粒度細化:用 ReentrantLock、StampedLock、分段鎖、ThreadLocal、無鎖結構替代大範圍 synchronized。

「過度同步」不僅是效能殺手,更是死鎖、活鎖、數據不一致的溫床。將同步區視為臨界區保護共享狀態的最後一道防線,而非業務邏輯的容器,是寫出高併發健壯 Java 代碼的關鍵心法。

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