CodeGym /課程 /JAVA 25 SELF /互斥鎖與信號量:語法與應用

互斥鎖與信號量:語法與應用

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

1. 互斥鎖(Mutex):是什麼以及如何運作

互斥鎖(英文「mutual exclusion」,意即「互斥」)是一種機制,讓同一時間只有一個執行緒能執行臨界區程式碼。若互斥鎖已被佔用(被其他執行緒取得),其餘執行緒會等待直到它被釋放。

在 Java 中,常以同步的目標物件充當互斥鎖:synchronized。自 Java 5 版起,出現了類別 ReentrantLock——更明確且更彈性的互斥鎖實作。

示意

想像一個只有一把鑰匙(互斥鎖)的房間。要進入就必須拿到鑰匙。若鑰匙不在(已被他人拿走),你只能在門外等待。一旦鑰匙被放回(互斥鎖被釋放),下一位就可以進入。

Java 中互斥鎖的語法

透過 synchronized(經典作法):

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

此處整個方法 increment 受互斥鎖保護——同一時間只有一個執行緒可以執行它。

透過 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(); // 取得互斥鎖
        try {
            count++;
        } finally {
            lock.unlock(); // 務必釋放!
        }
    }
}

重要!務必在 finally 區塊中釋放互斥鎖,否則可能出現「永久阻塞」(deadlock)——程式會掛住。

什麼時候需要互斥鎖?

當一次只能有一個執行緒使用某個資源時,就需要互斥鎖。該資源可以是變數、檔案或資料庫。當對資源的操作不是原子性時,使用互斥鎖尤為重要:即便是簡單的 count++,其實也包含三個步驟——讀取值、遞增並寫回。沒有互斥鎖,多個執行緒可能在步驟之間相互干擾,造成資料競爭。

2. 信號量(Semaphore):用途與運作方式

信號量是一種「調節器」,允許同時有多個執行緒使用資源,但不超過設定的數量。若達到上限,其餘執行緒會排隊等待。

類比:可停 3 輛車的停車場。車位滿了,後來的車必須等到有人離開。

Java 中信號量的語法

可使用類別 Semaphore,位於套件 java.util.concurrent

import java.util.concurrent.Semaphore;

public class ParkingLot {
    private final Semaphore spots;

    public ParkingLot(int places) {
        this.spots = new Semaphore(places);
    }

    public void parkCar(String car) throws InterruptedException {
        spots.acquire(); // 嘗試佔用車位(若沒有則等待)
        try {
            System.out.println(car + " 已停好.");
            Thread.sleep(1000); // 車子停在停車場
        } finally {
            spots.release(); // 釋放車位
            System.out.println(car + " 已離開.");
        }
    }
}

用法:

ParkingLot parking = new ParkingLot(3);

for (int i = 1; i <= 5; i++) {
    final String car = "車輛 " + i;
    new Thread(() -> {
        try {
            parking.parkCar(car);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

結果:同一時間停車場不會超過三輛車——其餘的會等待。

信號量如何運作?

  • 建立信號量時需指定「許可」(permits)的數量。
  • 方法 acquire() 嘗試取得許可:若有可用,執行緒通過;若沒有,則等待。
  • 方法 release() 將許可歸還。
  • 只有一個許可的信號量行為幾乎等同互斥鎖,但沒有「擁有者」。

3. 互斥鎖與信號量:有何差異?

特性 互斥鎖(Mutex) 信號量(Semaphore)
允許的執行緒數 僅一個 多個(受限數量)
用途 保護資源 限制存取(例如池)
API в Java
synchronized, Lock
Semaphore
管理方式 通常由「擁有者」 可由任一執行緒釋放
典型場景 共享計數器、物件 連線池、停車場、限流

- 互斥鎖——用於需要獨占存取的情況。
- 信號量——允許同時通過數個,但非全部。

類比:互斥鎖——只有一間隔間的洗手間;信號量——有三間隔間的洗手間。

4. 實務範例

範例 1:用互斥鎖保護臨界區

假設有一家共享的銀行,數個執行緒在帳戶間轉帳。這些操作必須是原子性的。

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

public class BankAccount {
    private int balance;
    private final Lock lock = new ReentrantLock();

    public BankAccount(int initial) {
        this.balance = initial;
    }

    public void deposit(int amount) {
        lock.lock();
        try {
            balance += amount;
        } finally {
            lock.unlock();
        }
    }

    public void withdraw(int amount) {
        lock.lock();
        try {
            if (balance >= amount) {
                balance -= amount;
            }
        } finally {
            lock.unlock();
        }
    }

    public int getBalance() {
        return balance;
    }
}

這裡所有對餘額的操作都由互斥鎖保護,以避免 race condition。

範例 2:用信號量限制存取

伺服器同時最多只能處理 2 個用戶端(例如受授權限制)。

import java.util.concurrent.Semaphore;

public class Server {
    private final Semaphore connections = new Semaphore(2);

    public void handleRequest(String client) throws InterruptedException {
        connections.acquire();
        try {
            System.out.println(client + " 已連上伺服器.");
            Thread.sleep(2000); // 模擬處理請求
        } finally {
            connections.release();
            System.out.println(client + " 已斷線.");
        }
    }
}

用法:

Server server = new Server();

for (int i = 1; i <= 5; i++) {
    final String client = "客戶端 " + i;
    new Thread(() -> {
        try {
            server.handleRequest(client);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

結果:伺服器同時處理的用戶端不會超過兩個。

5. 使用重點與注意事項

互斥鎖:一定要釋放!
非常重要的是,即使拋出例外,也不能忘記呼叫 unlock()(或離開同步區塊)。請使用 try-finally

lock.lock();
try {
    // 臨界區
} finally {
    lock.unlock();
}

若忘記,可能造成「永久阻塞」;其他執行緒將無限期等待。

信號量:可以替別人釋放嗎?
與互斥鎖不同,release() 可由任一執行緒呼叫,即使它沒有執行 acquire()。有時很方便,但也容易出錯——請遵守規範。

只有一個許可的 Semaphore = 互斥鎖?
幾乎是。但信號量沒有「擁有者」的概念:任何釋放都會增加許可計數。互斥鎖則必須由取得者自行釋放。

別把信號量當作池
信號量不是物件池,而只是「許可計數器」。它常被用來實作各種池(例如資料庫連線池),但本身不儲存任何物件。

6. 使用互斥鎖與信號量時的常見錯誤

錯誤 №1: 忘了呼叫 unlock/release。 如果你取得了互斥鎖或信號量,卻沒有呼叫 unlock()release(),其他執行緒可能會永遠卡住。務必使用 try-finally,確保即使在發生例外時也能釋放鎖。

錯誤 №2: 在錯誤的物件上進行同步。 如果在非所有執行緒共享的變數上進行同步(例如區域變數或字串常值),同步將不起作用。

錯誤 №3: 重複釋放. 對於信號量而言:若呼叫 release() 的次數多於 acquire(),許可數會超出上限。請確保配對一致!

錯誤 №4: 用信號量取代互斥鎖(或反之)。 若需要獨占存取,請使用互斥鎖(synchronizedLock)。若要限制同時運作的執行緒數量——請使用 Semaphore

錯誤 №5: 鎖持有時間過長。 執行緒持有互斥鎖或信號量的時間越長,其它執行緒等待的時間就越久。請盡量縮短臨界區內的工作時間。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION