在 Java 中使用互斥对象-Java快速入门教程
1. 概述
在本教程中,我们将看到在 Java 中实现互斥锁的不同方法。
2. 互斥体
在多线程应用程序中,两个或多个线程可能需要同时访问共享资源,从而导致意外行为。此类共享资源的示例包括数据结构、输入输出设备、文件和网络连接。
我们将此方案称为争用条件。并且,程序访问共享资源的部分称为关键部分。因此,为了避免竞争条件,我们需要同步对关键部分的访问。
互斥锁(或互斥)是最简单的同步器类型——它确保一次只有一个线程可以执行计算机程序的关键部分。
要访问关键部分,线程获取互斥锁,然后访问关键部分,最后释放互斥锁。同时,所有其他线程都会阻塞,直到互斥体释放。一旦一个线程退出关键部分,另一个线程就可以进入关键部分。
3. 为什么选择互斥锁?
首先,让我们举一个SequenceGeneraror类的例子,它通过每次将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. 使用同步关键字
首先,我们将讨论同步关键字,这是在 Java 中实现互斥锁的最简单方法。
Java 中的每个对象都有一个与之关联的内在锁。同步方法和同步块使用此内部锁将关键部分的访问限制为一次只能访问一个线程。
因此,当线程调用同步方法或进入同步块时,它会自动获取锁。当方法或块完成或从中引发异常时,锁将释放。
让我们将getNextSequence更改为具有互斥锁,只需添加同步关键字:
public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {
@Override
public synchronized int getNextSequence() {
return super.getNextSequence();
}
}
同步块类似于同步方法,对关键部分和我们可用于锁定的对象有更多的控制。
那么,现在让我们看看如何使用同步块在自定义互斥对象上进行同步:
public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {
private Object mutex = new Object();
@Override
public int getNextSequence() {
synchronized (mutex) {
return super.getNextSequence();
}
}
}
5. 使用重入锁
ReentrantLock类是在 Java 1.5 中引入的。它提供了比同步关键字方法更大的灵活性和控制力。
让我们看看如何使用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. 使用信号量
与ReentrantLock一样,信号量类也在Java 1.5中引入。
在互斥锁的情况下,只有一个线程可以访问关键部分,而信号量允许固定数量的线程访问关键部分。因此,我们还可以通过将信号灯中允许的线程数设置为 1 来实现互斥锁。
现在,让我们使用信号量创建另一个线程安全版本的序列生成器:
public class SequenceGeneratorUsingSemaphore extends SequenceGenerator {
private Semaphore mutex = new Semaphore(1);
@Override
public int getNextSequence() {
try {
mutex.acquire();
return super.getNextSequence();
} catch (InterruptedException e) {
// exception handling code
} finally {
mutex.release();
}
}
}
7. 使用 Guava 的监视器类
到目前为止,我们已经看到了使用 Java 提供的功能实现互斥锁的选项。
但是,Google的Guava库的Monitor类是ReentrantLock类的更好替代品。根据其文档,使用Monitor的代码比使用ReentrantLock 的代码更具可读性且更不容易出错。
首先,我们将为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 中实现它的不同方法。