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.concurrent 的 Semaphore 类:
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 | |
|
| 所有权/管理 | 通常有“持有者” | 可由任意线程释放 |
| 典型场景 | 共享计数器、对象 | 连接池、停车场、限流 |
- 互斥锁——用于需要独占访问的场景。
- 信号量——用于允许多个但不是所有线程并发的场景。
类比:互斥锁——只有一个隔间的洗手间;信号量——有三个隔间的洗手间。
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:把信号量当互斥锁(或反之)。 如果需要独占访问,请使用互斥锁(synchronized 或 Lock)。如果需要限制同时运行的线程数,请使用 Semaphore。
错误 5:长时间持有锁。 线程持有互斥锁或信号量的时间越长,其他线程等待的时间越久。尽量缩短在临界区内的执行时间。
GO TO FULL VERSION