Baeldung 翻译系列之Java并发基础:
Java中的concurrent包
Java中的Synchronized关键字
Future介绍
ThreadLocal介绍
Java线程的生命周期
如何杀掉一个Java线程
Java中的线程池介绍
实现Runnable接口还是继承Thread类
Java中的wait和notify方法
Runnable vs Callable
wait和sleep的区别
Thread.join方法介绍
Java中使用锁对象
ThreadPoolTaskExecutor中的corePoolSize和maxPoolSize
Java中的异步编程
1. 概览
本教程中,我们会看到在Java中实现 互斥锁(mutex
) 的不同方法
2. 互斥锁(Mutex)
在多线程应用程序中,两个或多个线程可能需要同时访问共享资源,从而导致意外行为。这些共享资源的示例包括数据结构、输入输出设备、文件和网络连接。
我们将这种情况称为竞态条件(race condition
)。程序中访问共享资源的部分被称为临界区(critical section
)。为了避免竞态条件,我们需要对对临界区的访问进行同步。
互斥锁(mutex
,或称为互斥量)是最简单的同步器类型,它确保一次只有一个线程可以执行计算机程序的临界区。
要访问临界区,线程先获取互斥锁,然后访问临界区,最后释放互斥锁。在此期间,所有其他线程都会阻塞,直到互斥锁被释放。一旦一个线程退出临界区,另一个线程就可以进入临界区。
3. 为何使用互斥锁?
首先,让我们以一个SequenceGenerator
类的示例开始,该类通过每次将currentValue
增加一来生成下一个序列:
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
允许一定数量的线程访问临界区。因此,我们也可以通过将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
的代码比使用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中实现互斥锁的不同方式。
评论区