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 中信号量的语法

为此使用来自包 java.util.concurrentSemaphore 类:

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)
可同时访问的线程数 只有一个 多个(受限数量)
用途 保护资源 限制并发访问(例如池)
Java 中的 API
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:长时间持有锁。 线程持有互斥锁或信号量的时间越长,其他线程等待的时间越久。尽量缩短在临界区内的执行时间。

1
任务
JAVA 25 SELF, 第 52 级, 课程 3
已锁定
关键事件监控 🚨
关键事件监控 🚨
1
任务
JAVA 25 SELF, 第 52 级, 课程 3
已锁定
跑道访问管理 ✈️
跑道访问管理 ✈️
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION