Kamlesh Kumar 2025-03-26
1. 概述
在本教程中,我们将探讨在 Java 中实现互斥锁(mutex)的不同方式。
2. 什么是互斥锁(Mutex)?
在多线程应用程序中,两个或多个线程可能同时访问共享资源,从而导致不可预期的行为。这类共享资源包括数据结构、输入/输出设备、文件和网络连接等。
我们称这种场景为竞态条件(race condition)。程序中访问共享资源的部分被称为临界区(critical section)。为了避免竞态条件,我们需要对临界区的访问进行同步。
互斥锁(mutex,即 mutual exclusion) 是最简单的同步器类型——它确保在任意时刻,只有一个线程可以执行程序中的临界区代码。
要访问临界区,线程首先获取互斥锁,然后执行临界区代码,最后释放互斥锁。在此期间,所有其他线程将被阻塞,直到该互斥锁被释放。一旦某个线程退出临界区,另一个线程就可以进入。
3. 为什么需要互斥锁?
首先,我们来看一个 SequenceGenerator 类的例子,它通过每次将 currentValue 加 1 来生成下一个序列号:
public class SequenceGenerator {
private int currentValue = 0;
public int getNextSequence() {
currentValue = currentValue + 1;
return currentValue;
}
}
现在,我们编写一个测试用例,观察当多个线程并发调用该方法时的行为:
@Test
public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception {
int count = 1000;
Set<Integer> uniqueSequences = getUniqueSequences(new SequenceGenerator(), count);
Assert.assertEquals(count, uniqueSequences.size());
}
private Set<Integer> getUniqueSequences(SequenceGenerator generator, int count) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
Set<Integer> uniqueSequences = new LinkedHashSet<>();
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 0; i < count; i++) {
futures.add(executor.submit(generator::getNextSequence));
}
for (Future<Integer> future : futures) {
uniqueSequences.add(future.get());
}
executor.awaitTermination(1, TimeUnit.SECONDS);
executor.shutdown();
return uniqueSequences;
}
运行该测试时,经常会失败,并提示类似以下错误:
java.lang.AssertionError: expected:<1000> but was:<989>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:645)
uniqueSequences 的大小应当等于我们在测试中调用 getNextSequence 方法的次数。但由于存在竞态条件,实际结果往往不一致。显然,这不是我们期望的行为。
因此,为了避免此类竞态条件,我们必须确保同一时间只有一个线程能执行 getNextSequence 方法。在这种情况下,我们可以使用互斥锁来同步线程。
Java 提供了多种实现互斥锁的方式。接下来,我们将逐一介绍如何为 SequenceGenerator 类实现互斥锁。
4. 使用 synchronized 关键字
首先,我们讨论 synchronized 关键字,这是 Java 中实现互斥锁最简单的方式。
Java 中的每个对象都有一个与之关联的内置锁(intrinsic lock)。synchronized 方法和 synchronized 代码块正是利用这个内置锁,来限制临界区在同一时间只能被一个线程访问。
当一个线程调用 synchronized 方法或进入 synchronized 代码块时,会自动获取该对象的锁;当方法或代码块执行完毕(或抛出异常)时,锁会被自动释放。
让我们通过添加 synchronized 关键字,使 getNextSequence 方法具备互斥能力:
public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {
@Override
public synchronized int getNextSequence() {
return super.getNextSequence();
}
}
synchronized 代码块与 synchronized 方法类似,但提供了对临界区范围以及用于加锁的对象更精细的控制。
下面展示如何使用 synchronized 代码块,在自定义的互斥对象上进行同步:
public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {
private Object mutex = new Object();
@Override
public int getNextSequence() {
synchronized (mutex) {
return super.getNextSequence();
}
}
}
5. 使用 ReentrantLock
ReentrantLock 类从 Java 1.5 开始引入,相比 synchronized 关键字,它提供了更高的灵活性和控制能力。
下面我们展示如何使用 ReentrantLock 实现互斥:
public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator {
private ReentrantLock mutex = new ReentrantLock();
@Override
public int getNextSequence() {
try {
mutex.lock();
return super.getNextSequence();
} finally {
mutex.unlock();
}
}
}
6. 使用 Semaphore
与 ReentrantLock 一样,Semaphore 类也是在 Java 1.5 中引入的。
虽然互斥锁只允许一个线程访问临界区,但信号量(Semaphore)允许多个(固定数量的)线程同时访问。因此,我们也可以通过将信号量的许可数量设为 1 来实现互斥锁。
下面使用 Semaphore 创建另一个线程安全的 SequenceGenerator 版本:
public class SequenceGeneratorUsingSemaphore extends SequenceGenerator {
private Semaphore mutex = new Semaphore(1);
@Override
public int getNextSequence() {
try {
mutex.acquire();
return super.getNextSequence();
} catch (InterruptedException e) {
// 异常处理代码
} finally {
mutex.release();
}
}
}
7. 使用 Guava 的 Monitor 类
到目前为止,我们已经介绍了 Java 自带的互斥锁实现方式。
然而,Google Guava 库中的 Monitor 类是 ReentrantLock 的一个更优替代方案。根据其官方文档说明,使用 Monitor 编写的代码更具可读性,也更不容易出错。
首先,我们在项目中添加 Guava 的 Maven 依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
接着,我们使用 Monitor 类编写另一个 SequenceGenerator 的子类:
public class SequenceGeneratorUsingMonitor extends SequenceGenerator {
private Monitor mutex = new Monitor();
@Override
public int getNextSequence() {
mutex.enter();
try {
return super.getNextSequence();
} finally {
mutex.leave();
}
}
}
8. 总结
在本教程中,我们深入理解了互斥锁的概念,并探讨了在 Java 中实现互斥锁的多种方式,包括:
synchronized关键字(方法和代码块)ReentrantLockSemaphore- Guava 的
Monitor类
每种方式各有优劣,开发者可根据具体需求选择最适合的方案。