1. 概述
本文将探讨在Java中实现互斥锁(mutex)的多种方式。互斥锁是并发编程中的基础概念,掌握不同实现方式能帮助我们在实际开发中灵活应对各种场景。
2. 互斥锁
在多线程应用中,当两个或多个线程同时访问共享资源时,会导致不可预期的行为。典型的共享资源包括:
- 数据结构
- I/O设备
- 文件
- 网络连接
这种场景被称为竞态条件(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 testRaceCondition() throws InterruptedException {
int threadCount = 1000;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
SequenceGenerator generator = new SequenceGenerator();
Set<Integer> uniqueSequences = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < threadCount; i++) {
executor.execute(() -> {
uniqueSequences.add(generator.getNextSequence());
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
assertEquals(threadCount, uniqueSequences.size());
}
执行测试会发现它经常失败,错误类似:
Expected :1000
Actual :999
uniqueSequences
的大小本应等于方法调用次数,但由于竞态条件导致结果不一致。为避免这种问题,必须确保同一时间只有一个线程执行getNextSequence
方法。这时互斥锁就派上用场了。
接下来我们将用不同方式为SequenceGenerator
实现互斥锁。
4. 使用synchronized
关键字
synchronized
关键字是Java中最简单的互斥实现方式。每个Java对象都有一个内置锁(intrinsic lock),**synchronized
方法和代码块都利用这个锁**来保证临界区的线程安全。
当线程调用synchronized
方法或进入synchronized
块时,会自动获取锁。方法/块执行结束或抛出异常时锁自动释放。
同步方法版
只需给getNextSequence
添加synchronized
关键字:
public synchronized int getNextSequence() {
currentValue = currentValue + 1;
return currentValue;
}
同步代码块版
同步代码块更灵活,能精确控制临界区范围和锁对象:
private final Object mutex = new Object();
public int getNextSequence() {
synchronized (mutex) {
currentValue = currentValue + 1;
return currentValue;
}
}
5. 使用ReentrantLock
Java 1.5引入的ReentrantLock
类比synchronized
提供更强的灵活性和控制力。使用示例:
import java.util.concurrent.locks.ReentrantLock;
public class SequenceGeneratorWithLock extends SequenceGenerator {
private final ReentrantLock lock = new ReentrantLock();
@Override
public int getNextSequence() {
lock.lock();
try {
currentValue = currentValue + 1;
return currentValue;
} finally {
lock.unlock();
}
}
}
关键点:
- 必须手动调用
lock()
和unlock()
- 最佳实践是将
unlock()
放在finally
块中 - 支持公平锁、超时获取等高级特性
6. 使用Semaphore
Semaphore
(信号量)同样在Java 1.5引入。与互斥锁(只允许一个线程访问临界区)不同,信号量允许固定数量的线程访问临界区。因此将信号量的许可数设为1就能实现互斥锁。
实现示例:
import java.util.concurrent.Semaphore;
public class SequenceGeneratorWithSemaphore extends SequenceGenerator {
private final Semaphore semaphore = new Semaphore(1);
@Override
public int getNextSequence() {
try {
semaphore.acquire();
currentValue = currentValue + 1;
return currentValue;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted", e);
} finally {
semaphore.release();
}
}
}
注意:
acquire()
可能抛出InterruptedException
,需妥善处理- 必须确保
release()
在finally
中执行 - 适合需要控制并发访问数量的场景
7. 使用Guava的Monitor
类
前几种都是Java原生方案,Google Guava库提供的Monitor
类是ReentrantLock
的更优替代品。根据官方文档,使用Monitor
的代码比ReentrantLock
更易读且不易出错。
首先添加Maven依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
实现示例:
import com.google.common.util.concurrent.Monitor;
public class SequenceGeneratorWithMonitor extends SequenceGenerator {
private final Monitor monitor = new Monitor();
@Override
public int getNextSequence() {
monitor.enter();
try {
currentValue = currentValue + 1;
return currentValue;
} finally {
monitor.leave();
}
}
}
优势:
- 语法更简洁(无需显式锁操作)
- 内置超时和中断检测
- 支持条件变量(Condition)
- 推荐在复杂同步场景中使用
8. 结论
本文深入探讨了互斥锁的概念,并展示了在Java中的四种实现方式:
synchronized
关键字:最简单直接,适合基础场景- **
ReentrantLock
**:功能强大,需手动管理锁 - **
Semaphore
**:灵活控制并发访问数 - **Guava的
Monitor
**:易读性强,推荐用于复杂逻辑
每种方案都有适用场景,实际开发中需根据需求权衡:
- 简单场景用
synchronized
足矣 - 需要公平锁/超时机制时选
ReentrantLock
- 需要控制并发数量时用
Semaphore
- 复杂同步逻辑优先考虑
Monitor
完整示例代码见 GitHub仓库。