在多线程编程中,如何有效地控制多个线程对共享资源的访问是一个重要的问题。Semaphore
(信号量)是 Java 中一个重要的同步工具,用于控制同时访问特定资源的线程数量。本文将深入探讨 Semaphore
的产生背景、基本使用、工作原理及其广泛的应用场景。
Semaphore 产生的背景
Semaphore
的概念由荷兰计算机科学家 Edsger W. Dijkstra 在 1960 年代提出,旨在解决多线程编程中的同步问题,即如何在多个线程或进程之间协调对共享资源的访问。
多线程同步问题
在多线程环境中,多个线程可能会同时访问共享资源,这会导致数据不一致或竞态条件。常见的同步问题包括:
- 互斥访问:确保同一时刻只有一个线程可以访问共享资源。
- 读写锁:允许多个线程同时读取资源,但写入资源时需要独占访问。
- 信号量控制:限制并发访问的线程数,避免资源过载。
早期解决方案
在 Semaphore
被提出之前,早期的解决方案主要依赖于锁(Lock)和条件变量(Condition Variable)。这些同步原语可以解决基本的互斥访问问题,但在控制并发线程数量方面存在一定的局限性。
信号量的引入
为了解决上述问题,Dijkstra 引入了信号量的概念。信号量是一种计数器,用于控制对共享资源的访问。信号量可以是二进制信号量(类似于互斥锁)或计数信号量(允许多个线程同时访问资源)。
信号量的基本操作包括:
- P 操作(Proberen,试图减少):尝试减少信号量,如果信号量为 0,则线程进入等待状态。
- V 操作(Verhogen,增加):增加信号量,并唤醒等待队列中的一个线程。
信号量的实现
在 Java 中,信号量通过 Semaphore
类实现。Semaphore
提供了获取和释放许可的方法,用于控制线程对共享资源的并发访问。
现代应用
在现代计算机系统中,Semaphore
的应用场景非常广泛,包括但不限于:
- 限流控制:限制同时访问某个服务或资源的线程数。
- 资源池管理:控制对有限资源(如数据库连接、线程池)的并发访问。
- 生产者-消费者模型:协调生产者和消费者线程之间的数据传递。
- 并发任务执行:限制同时执行的并发任务数量,避免系统过载。
Semaphore 的基本使用
假设有 N (N>5) 个线程需要访问一个共享资源,但同一时刻只能有 5 个线程能够访问这个资源。以下是一个简单的示例:
java
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
// 初始共享资源数量
final Semaphore semaphore = new Semaphore(5);
// 模拟 N 个线程
for (int i = 0; i < 10; i++) {
new Thread(new Task(semaphore)).start();
}
}
static class Task implements Runnable {
private Semaphore semaphore;
Task(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
try {
// 获取1个许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获取了许可");
// 模拟任务执行
Thread.sleep(2000);
// 释放1个许可
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 释放了许可");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在这个示例中,同一时刻最多只有 5 个线程能够获取到许可并执行任务,其他线程会被阻塞,直到有线程释放许可。
Semaphore 的构造方法
Semaphore
有两个构造方法:
java
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
permits
:表示许可的数量。fair
:表示是否是公平模式。
公平模式下,调用 acquire()
方法的顺序就是获取许可的顺序,遵循 FIFO;非公平模式下则是抢占式的。
Semaphore 的工作原理
Semaphore
是共享锁的一种实现,其核心在于内部的 state
值,这个值表示当前剩余的许可数量。以下是 acquire
和 release
方法的源码解析:
acquire 方法
java
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
acquire()
方法尝试获取一个许可(arg=1
)。tryAcquireShared(arg)
方法尝试减少state
的值,如果state >= arg
则表示获取成功,否则线程会被阻塞。
tryAcquireShared 方法
java
protected int tryAcquireShared(int arg) {
for (;;) {
int available = getState();
int remaining = available - arg;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
getState()
获取当前剩余的许可数量。compareAndSetState(available, remaining)
使用 CAS 操作尝试修改state
的值。
release 方法
java
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
release()
方法尝试释放一个许可(arg=1
)。tryReleaseShared(arg)
方法增加state
的值,并唤醒同步队列中的一个线程。
tryReleaseShared 方法
java
protected boolean tryReleaseShared(int arg) {
for (;;) {
int current = getState();
int next = current + arg;
if (compareAndSetState(current, next))
return true;
}
}
getState()
获取当前剩余的许可数量。compareAndSetState(current, next)
使用 CAS 操作尝试修改state
的值。
Semaphore 的应用场景
1. 限流控制
场景描述
在高并发系统中,限流控制是非常重要的一环。限流可以保护系统避免因瞬时高并发请求导致的过载,从而提高系统的稳定性。
示例代码
假设我们有一个 Web 服务,最多允许 10 个并发请求。使用 Semaphore
来实现限流控制:
java
import java.util.concurrent.Semaphore;
public class WebService {
// 定义一个信号量,初始许可数量为10
private final Semaphore semaphore = new Semaphore(10);
public void handleRequest(String request) {
try {
// 获取一个许可,如果没有许可则会阻塞等待
semaphore.acquire();
System.out.println("Handling request: " + request);
// 模拟处理请求
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放一个许可
semaphore.release();
}
}
public static void main(String[] args) {
WebService service = new WebService();
// 模拟20个请求
for (int i = 0; i < 20; i++) {
final int requestId = i;
new Thread(() -> service.handleRequest("Request " + requestId)).start();
}
}
}
在这个示例中,我们定义了一个 Semaphore
,初始许可数量为 10。每次处理请求时,先获取一个许可,如果没有许可则会阻塞等待。处理完请求后,释放一个许可。通过这种方式,我们可以有效地控制并发请求的数量,避免系统过载。
2. 资源池管理
场景描述
在资源有限的情况下,比如数据库连接池、线程池等,需要控制对这些有限资源的并发访问。Semaphore
可以很好地管理这些资源的分配。
示例代码
假设我们有一个数据库连接池,最多允许 5 个并发连接:
java
import java.util.concurrent.Semaphore;
public class ConnectionPool {
private final Semaphore semaphore = new Semaphore(5);
public void getConnection() {
try {
// 获取一个许可,如果没有许可则会阻塞等待
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 获取了数据库连接");
// 模拟使用数据库连接
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放一个许可
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 释放了数据库连接");
}
}
public static void main(String[] args) {
ConnectionPool pool = new ConnectionPool();
// 模拟10个线程获取数据库连接
for (int i = 0; i < 10; i++) {
new Thread(pool::getConnection).start();
}
}
}
在这个示例中,我们定义了一个 Semaphore
,初始许可数量为 5。每次获取数据库连接时,先获取一个许可,如果没有许可则会阻塞等待。使用完数据库连接后,释放一个许可。通过这种方式,我们可以有效地管理数据库连接的分配。
3. 生产者-消费者模型
场景描述
生产者-消费者模型是一种经典的多线程协作模型。生产者负责生产数据,消费者负责消费数据。使用 Semaphore
可以很好地协调生产者和消费者之间的数据传递。
示例代码
假设我们有一个生产者-消费者模型,生产者生产数据,消费者消费数据:
java
import java.util.concurrent.Semaphore;
import java.util.Queue;
import java.util.LinkedList;
public class ProducerConsumer {
private final Semaphore items = new Semaphore(0); // 表示可消费的项目数量
private final Semaphore spaces = new Semaphore(10); // 表示可生产的空间数量
private final Queue<Integer> queue = new LinkedList<>();
public void produce(int item) throws InterruptedException {
spaces.acquire(); // 获取一个生产许可
synchronized (this) {
queue.add(item);
System.out.println("Produced: " + item);
}
items.release(); // 释放一个消费许可
}
public int consume() throws InterruptedException {
items.acquire(); // 获取一个消费许可
int item;
synchronized (this) {
item = queue.poll();
System.out.println("Consumed: " + item);
}
spaces.release(); // 释放一个生产许可
return item;
}
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
// 创建生产者线程
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
pc.produce(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 创建消费者线程
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
pc.consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
在这个示例中,我们定义了两个 Semaphore
:
items
:表示可消费的项目数量,初始值为 0。spaces
:表示可生产的空间数量,初始值为 10。
生产者线程在生产数据时,先获取一个生产许可,生产完数据后,释放一个消费许可。消费者线程在消费数据时,先获取一个消费许可,消费完数据后,释放一个生产许可。通过这种方式,我们可以有效地协调生产者和消费者之间的数据传递。
4. 并发任务执行
场景描述
在一些任务调度系统中,可能需要限制同时执行的并发任务数量,以避免系统过载。Semaphore
可以很好地控制并发任务的执行数量。
示例代码
假设我们有一个任务调度系统,最多允许 3 个并发任务:
java
import java.util.concurrent.Semaphore;
public class TaskScheduler {
private final Semaphore semaphore = new Semaphore(3);
public void executeTask(String task) {
try {
semaphore.acquire(); // 获取一个许可,如果没有许可则会阻塞等待
System.out.println("Executing task: " + task);
// 模拟执行任务
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放一个许可
}
}
public static void main(String[] args) {
TaskScheduler scheduler = new TaskScheduler();
// 模拟10个任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
new Thread(() -> scheduler.executeTask("Task " + taskId)).start();
}
}
}
在这个示例中,我们定义了一个 Semaphore
,初始许可数量为 3。每次执行任务时,先获取一个许可,如果没有许可则会阻塞等待。任务执行完毕后,释放一个许可。通过这种方式,我们可以有效地控制并发任务的执行数量。
结论
通过本文的讲解,我们深入了解了 Semaphore
的产生背景、基本使用、构造方法、工作原理以及其典型的应用场景。Semaphore
是一个非常强大的工具,可以很好地控制资源的并发访问,适用于多种实际应用场景,如限流控制、资源池管理、生产者-消费者模型和并发任务执行等。在实际开发中,合理使用 Semaphore
可以极大地提高系统的稳定性和并发处理能力,是多线程编程中不可或缺的工具之一。