並行處理模式(Java)

設計與模擬優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門 更新於 Oct 9, 2026

範圍 — L4 以上會出現的少數幾題 Java 並行題:依序列印、生產者/消費者、讀寫協調與死結避免,以及它們會用到的同步原語。 另見:python_gotchas.md — GIL 與 Python 的並行故事;design.md — 執行緒安全的結構設計;java_trick.md — Java 容器慣用手法。

LeetCode 題目清單

總覽

Google 在 L4 以上偶爾會考並行/多執行緒題。這類題目在檢驗你對同步機制、執行緒安全,以及並行資料結構設計的理解。

什麼時候用

  • 題目明講了執行緒、平行執行,或生產者/消費者
  • 要求設計執行緒安全資料結構的設計題

模式 1:依序列印/順序控制

用 CountDownLatch — LC 1114

java
// LC 1114 — Print in Order
class Foo {
    private CountDownLatch latch1 = new CountDownLatch(1);
    private CountDownLatch latch2 = new CountDownLatch(1);

    public void first(Runnable printFirst) throws InterruptedException {
        printFirst.run();
        latch1.countDown();
    }

    public void second(Runnable printSecond) throws InterruptedException {
        latch1.await();
        printSecond.run();
        latch2.countDown();
    }

    public void third(Runnable printThird) throws InterruptedException {
        latch2.await();
        printThird.run();
    }
}

用 Semaphore — LC 1115

java
// LC 1115 — Print FooBar Alternately
class FooBar {
    private int n;
    private Semaphore fooSem = new Semaphore(1);
    private Semaphore barSem = new Semaphore(0);

    public FooBar(int n) { this.n = n; }

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            fooSem.acquire();
            printFoo.run();
            barSem.release();
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            barSem.acquire();
            printBar.run();
            fooSem.release();
        }
    }
}

模式 2:生產者-消費者/有界緩衝區

java
// LC 1188 — Design Bounded Blocking Queue
class BoundedBlockingQueue {
    private Queue<Integer> queue = new LinkedList<>();
    private int capacity;
    private ReentrantLock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();

    public BoundedBlockingQueue(int capacity) { this.capacity = capacity; }

    public void enqueue(int element) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) notFull.await();
            queue.offer(element);
            notEmpty.signal();
        } finally { lock.unlock(); }
    }

    public int dequeue() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) notEmpty.await();
            int val = queue.poll();
            notFull.signal();
            return val;
        } finally { lock.unlock(); }
    }

    public int size() { lock.lock(); try { return queue.size(); } finally { lock.unlock(); } }
}

模式 3:讀寫鎖/H2O 問題

java
// LC 1117 — Building H2O
class H2O {
    private Semaphore hSem = new Semaphore(2);
    private Semaphore oSem = new Semaphore(0);
    private CyclicBarrier barrier = new CyclicBarrier(3, () -> {
        hSem.release(2);
    });

    public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
        hSem.acquire();
        releaseHydrogen.run();
        try { barrier.await(); } catch (BrokenBarrierException e) {}
    }

    public void oxygen(Runnable releaseOxygen) throws InterruptedException {
        // NOTE: sketch only — oSem is initialized to 0 permits and never released,
        // so this acquire() blocks forever. A correct H2O gates O with its own
        // Semaphore(1) (limit one O in flight) and lets the CyclicBarrier(3)
        // rendezvous 2 H + 1 O; the barrier action then release(2)s hSem.
        oSem.acquire();
        releaseOxygen.run();
        try { barrier.await(); } catch (BrokenBarrierException e) {}
    }
}

模式 4:死結 — 打破四個條件之一

死結需要四個條件同時成立,所以只要阻止其中任何一個就夠了:

條件 意義 怎麼消除
互斥(Mutual exclusion) 一把鎖同一時間只被一個執行緒持有 幾乎無法消除 —— 這正是鎖的用途
持有並等待(Hold and wait) 執行緒握著一把鎖,同時又要求另一把 一開始就取得全部,或在要求前先釋放
不可搶占(No preemption) 鎖無法被奪走 tryLock(timeout) —— 取得失敗時先釋放已持有的鎖,退讓後重試,而不是一直等
循環等待(Circular wait) 「誰在等誰」形成一個環 全域的加鎖順序,或限制競爭者的數量

面試時要攻擊的是循環等待:它最便宜就能強制執行,也最容易論證正確性。

標準解法 — 全域加鎖順序

給每把鎖一個固定的等級(id、位址,任何全序且穩定的東西),並讓每個執行緒都依等級遞增的順序取得。 要形成環,必須有人握著高等級的鎖去等低等級的鎖,而這條規則禁止這麼做 —— 所以環不可能形成。

java
// java
// Transfer between two accounts without deadlocking against the reverse transfer
// IDEA: order the two locks by a stable id, so t1 (A->B) and t2 (B->A) both take
//       the lower id first and one of them simply waits
void transfer(Account from, Account to, long amount) {
    Account first  = from.id < to.id ? from : to;     // total order over the locks
    Account second = from.id < to.id ? to   : from;
    synchronized (first) {
        synchronized (second) {
            from.debit(amount);
            to.credit(amount);
        }
    }
}

CtCI 15.4 問的是預防版本:讓執行緒事先宣告它會需要哪些鎖,建立「先取得於」(acquires-before)圖, 並拒絕任何會讓圖閉合成環的宣告。同樣的想法,只是在註冊時檢查,而不是靠慣例強制。

哲學家就餐 — LC 1226

五位哲學家、五支叉子,每人需要身旁的兩支。每個人都先拿左手邊的叉子,正好就是循環等待。 兩個一行就能搞定的解法:

java
// java
// LC 1226 — The Dining Philosophers
// IDEA: a Semaphore(4) means at most 4 philosophers ever hold a fork, so at least one
//       always finds both free — the cycle can never close
// time = O(1) per meal, space = O(n)
class DiningPhilosophers {
    private final ReentrantLock[] forks = new ReentrantLock[5];
    private final Semaphore room = new Semaphore(4);   // n - 1 diners

    public DiningPhilosophers() {
        for (int i = 0; i < 5; i++) forks[i] = new ReentrantLock();
    }

    public void wantsToEat(int philosopher,
                           Runnable pickLeftFork,  Runnable pickRightFork,
                           Runnable eat,
                           Runnable putLeftFork,   Runnable putRightFork)
            throws InterruptedException {
        int left = philosopher, right = (philosopher + 1) % 5;

        room.acquire();
        forks[left].lock();
        forks[right].lock();

        pickLeftFork.run(); pickRightFork.run(); eat.run();
        putLeftFork.run();  putRightFork.run();

        forks[right].unlock();
        forks[left].unlock();
        room.release();
    }
}

另一種效果相同的做法:先拿 forks[min(left, right)] —— 也就是上面的全域順序解法。 第三個選項是讓奇數號的哲學家先拿右邊,打破造成環的對稱性。

模式 5:接力棒傳遞 — 誰做完就喚醒正確的下一位

當超過兩個角色交錯執行時,不要讓每個執行緒去輪詢共享計數器。給每個角色各自的 semaphore, 讓剛印完的執行緒只釋放擁有下一個值的那一個 —— 沒有任何執行緒會去檢查別人的狀態。

java
// java
// LC 1195 — Fizz Buzz Multithreaded
// IDEA: one semaphore per role, all starting at 0 except number(). After printing i,
//       hand the permit to whoever owns i+1, so the order is enforced by construction.
// time = O(n) total, space = O(1)
class FizzBuzz {
    private final int n;
    private final Semaphore num      = new Semaphore(1);   // 1 owns the first turn
    private final Semaphore fizz     = new Semaphore(0);
    private final Semaphore buzz     = new Semaphore(0);
    private final Semaphore fizzbuzz = new Semaphore(0);

    public FizzBuzz(int n) { this.n = n; }

    public void fizz(Runnable printFizz) throws InterruptedException {
        for (int i = 3; i <= n; i += 3) {
            if (i % 5 == 0) continue;                      // 15s belong to fizzbuzz()
            fizz.acquire();
            printFizz.run();
            handOff(i + 1);
        }
    }

    public void buzz(Runnable printBuzz) throws InterruptedException {
        for (int i = 5; i <= n; i += 5) {
            if (i % 3 == 0) continue;
            buzz.acquire();
            printBuzz.run();
            handOff(i + 1);
        }
    }

    public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
        for (int i = 15; i <= n; i += 15) {
            fizzbuzz.acquire();
            printFizzBuzz.run();
            handOff(i + 1);
        }
    }

    public void number(Runnable printNumber) throws InterruptedException {
        for (int i = 1; i <= n; i++) {
            if (i % 3 == 0 || i % 5 == 0) continue;
            num.acquire();
            printNumber.run();
            handOff(i + 1);
        }
    }

    /** Wake the single thread that owns value i. */
    private void handOff(int i) {
        if (i > n) {                                       // past the end: wake everyone
            num.release(); fizz.release();                 // so nobody is left blocked
            buzz.release(); fizzbuzz.release();
        } else if (i % 15 == 0) fizzbuzz.release();
        else if (i % 3 == 0)    fizz.release();
        else if (i % 5 == 0)    buzz.release();
        else                    num.release();
    }
}

每個迴圈只走訪自己角色擁有的值,而任一時刻恰好只有一張許可在流通 —— 所以列印順序在結構上就是正確的, 沒有需要保護的共享可變計數器。LC 1114 和 LC 1116 是同一根接力棒,只是角色比較少。

重要並行原語

原語 用途 主要方法
synchronized 互斥 wait()、notify()、notifyAll()
ReentrantLock 可搭配條件變數的顯式鎖 lock()、unlock()、newCondition()
Semaphore 計數式許可 acquire()、release()
CountDownLatch 一次性閘門(數到 0 就開) await()、countDown()
CyclicBarrier 可重複使用的會合點 await()
volatile 可見性保證 —
AtomicInteger 免鎖計數器 incrementAndGet()、compareAndSet()

LC 範例

# 題目 關鍵概念
1114 Print in Order CountDownLatch/Semaphore
1115 Print FooBar Alternately 一對 Semaphore
1116 Print Zero Even Odd Semaphore 協調
1117 Building H2O CyclicBarrier + Semaphore
1188 Bounded Blocking Queue ReentrantLock + Condition
1195 Fizz Buzz Multithreaded Semaphore/CyclicBarrier
1226 The Dining Philosophers 避免死鎖