深度揭秘 Java Semaphore:从源码洞悉其使用原理
一、引言
在 Java 并发编程的领域中,合理地管理和控制资源的访问是至关重要的。当多个线程同时访问有限的资源时,如果不加以控制,就可能会出现资源竞争、数据不一致等问题。Java 提供了一系列强大的并发工具类来帮助开发者解决这些问题,其中 Semaphore (信号量)就是一个非常实用的工具。
Semaphore 可以用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理地使用公共资源。在本文中,我们将从源码层面深入剖析 Java Semaphore 的使用原理,详细解读其每一个关键步骤的实现细节,帮助你更好地理解和运用这个强大的工具。
二、Semaphore 概述
2.1 基本概念
Semaphore 是一种基于计数的信号量,它维护了一个许可(permit)的计数。每个线程在访问共享资源之前,必须先从 Semaphore 获取一个许可,如果许可的数量大于 0,线程可以获取许可并继续执行,同时许可的数量减 1;如果许可的数量为 0,线程将被阻塞,直到有其他线程释放许可。当线程使用完资源后,需要将许可归还给 Semaphore,许可的数量加 1。
2.2 核心方法
Semaphore 类提供了一系列核心方法,下面是一些常用方法的介绍:
Semaphore(int permits):构造函数,用于创建一个具有指定初始许可数量的Semaphore实例。Semaphore(int permits, boolean fair):构造函数,除了指定初始许可数量外,还可以指定是否使用公平模式。公平模式下,线程将按照请求许可的顺序依次获取许可。void acquire():当前线程尝试获取一个许可,如果许可数量大于 0,则获取成功并将许可数量减 1;如果许可数量为 0,则线程被阻塞,直到有其他线程释放许可。void acquire(int permits):当前线程尝试获取指定数量的许可,如果许可数量足够,则获取成功并将许可数量减去相应的值;如果许可数量不足,则线程被阻塞,直到有足够的许可被释放。void release():当前线程释放一个许可,许可数量加 1。如果有其他线程正在等待许可,会唤醒其中一个等待的线程。void release(int permits):当前线程释放指定数量的许可,许可数量加上相应的值。如果有其他线程正在等待许可,会唤醒足够数量的等待线程。boolean tryAcquire():当前线程尝试获取一个许可,如果许可数量大于 0,则获取成功并返回true;否则返回false,线程不会被阻塞。boolean tryAcquire(int permits):当前线程尝试获取指定数量的许可,如果许可数量足够,则获取成功并返回true;否则返回false,线程不会被阻塞。boolean tryAcquire(long timeout, TimeUnit unit):当前线程在指定的时间内尝试获取一个许可,如果在超时时间内许可数量大于 0,则获取成功并返回true;否则返回false。boolean tryAcquire(int permits, long timeout, TimeUnit unit):当前线程在指定的时间内尝试获取指定数量的许可,如果在超时时间内许可数量足够,则获取成功并返回true;否则返回false。
2.3 简单示例
下面是一个简单的示例,展示了 Semaphore 的基本使用:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
// 定义一个 Semaphore 实例,初始许可数量为 2
private static final Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
// 创建并启动 5 个线程
for (int i = 0; i < 5; i++) {
new Thread(new Worker(i)).start();
}
}
static class Worker implements Runnable {
private final int id;
public Worker(int id) {
this.id = id;
}
@Override
public void run() {
try {
// 线程尝试获取一个许可
semaphore.acquire();
System.out.println("线程 " + id + " 已获取许可,开始执行任务...");
// 模拟任务执行时间
Thread.sleep(2000);
System.out.println("线程 " + id + " 任务执行完毕,释放许可");
// 线程释放一个许可
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在这个示例中,我们创建了一个 Semaphore 实例,初始许可数量为 2。然后创建并启动了 5 个线程,每个线程在执行任务之前会尝试获取一个许可。由于许可数量有限,最多只有 2 个线程可以同时获取许可并执行任务,其他线程会被阻塞,直到有线程释放许可。
三、Semaphore 源码结构分析
3.1 类定义与继承关系
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class Semaphore implements java.io.Serializable {
// 内部同步器,继承自 AbstractQueuedSynchronizer
private final Sync sync;
// 抽象内部类,继承自 AbstractQueuedSynchronizer
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
// 构造函数,设置初始许可数量
Sync(int permits) {
setState(permits);
}
// 获取当前许可数量
final int getPermits() {
return getState();
}
// 尝试减少许可数量
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取当前许可数量
int available = getState();
// 计算剩余许可数量
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
// 尝试释放许可
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 获取当前许可数量
int current = getState();
// 计算新的许可数量
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
// 减少指定数量的许可
final void reducePermits(int reductions) {
for (;;) {
// 获取当前许可数量
int current = getState();
// 计算新的许可数量
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
if (compareAndSetState(current, next))
return;
}
}
// 获取并重置许可数量
final int drainPermits() {
for (;;) {
// 获取当前许可数量
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
}
// 非公平同步器,继承自 Sync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
// 构造函数,调用父类构造函数设置初始许可数量
NonfairSync(int permits) {
super(permits);
}
// 尝试获取共享锁
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
// 公平同步器,继承自 Sync
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
// 构造函数,调用父类构造函数设置初始许可数量
FairSync(int permits) {
super(permits);
}
// 尝试获取共享锁
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
// 构造函数,使用非公平模式
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
// 构造函数,可指定是否使用公平模式
public Semaphore(int permits, boolean fair) {
sync = fair? new FairSync(permits) : new NonfairSync(permits);
}
// 获取一个许可
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 获取指定数量的许可
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
// 尝试获取一个许可
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
// 尝试获取指定数量的许可
public boolean tryAcquire(int permits) {
if (permits < 0) throw new IllegalArgumentException();
return sync.nonfairTryAcquireShared(permits) >= 0;
}
// 在指定时间内尝试获取一个许可
public boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// 在指定时间内尝试获取指定数量的许可
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}
// 释放一个许可
public void release() {
sync.releaseShared(1);
}
// 释放指定数量的许可
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
// 获取当前许可数量
public int availablePermits() {
return sync.getPermits();
}
// 获取并重置许可数量
public int drainPermits() {
return sync.drainPermits();
}
// 减少指定数量的许可
protected void reducePermits(int reduction) {
if (reduction < 0) throw new IllegalArgumentException();
sync.reducePermits(reduction);
}
// 判断是否使用公平模式
public boolean isFair() {
return sync instanceof FairSync;
}
// 判断是否有线程在等待许可
public final boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
// 获取等待许可的线程数量
public final int getQueueLength() {
return sync.getQueueLength();
}
// 获取等待许可的线程集合
protected Collection<Thread> getQueuedThreads() {
return sync.getQueuedThreads();
}
}
从上述源码可以看出,Semaphore 类主要包含以下几个部分:
Sync抽象内部类:继承自AbstractQueuedSynchronizer(AQS),是Semaphore的核心同步器,定义了获取和释放许可的基本操作。NonfairSync内部类:继承自Sync,实现了非公平模式下的许可获取逻辑。FairSync内部类:继承自Sync,实现了公平模式下的许可获取逻辑。- 构造函数:提供了两种构造函数,一种使用非公平模式,另一种可以指定是否使用公平模式。
- 核心方法:提供了获取和释放许可的方法,以及一些辅助方法,如获取当前许可数量、判断是否使用公平模式等。
3.2 关键成员变量
sync:类型为Sync,是Semaphore的核心同步器,负责处理许可的获取和释放操作。state:在 AQS 中定义的一个整型变量,用于表示同步状态。在Semaphore中,state的值就是当前许可的数量。
四、核心方法源码分析
4.1 构造函数
// 构造函数,使用非公平模式
public Semaphore(int permits) {
// 创建非公平同步器实例
sync = new NonfairSync(permits);
}
// 构造函数,可指定是否使用公平模式
public Semaphore(int permits, boolean fair) {
// 根据 fair 参数选择创建公平同步器或非公平同步器实例
sync = fair? new FairSync(permits) : new NonfairSync(permits);
}
Semaphore 提供了两个构造函数,一个使用非公平模式,另一个可以指定是否使用公平模式。在构造函数中,会根据传入的参数创建相应的同步器实例,并将初始许可数量传递给同步器的构造函数。
4.2 acquire() 方法
// 获取一个许可
public void acquire() throws InterruptedException {
// 调用同步器的 acquireSharedInterruptibly 方法,尝试获取一个共享锁
sync.acquireSharedInterruptibly(1);
}
// 获取指定数量的许可
public void acquire(int permits) throws InterruptedException {
// 检查传入的许可数量是否小于 0,如果是则抛出异常
if (permits < 0) throw new IllegalArgumentException();
// 调用同步器的 acquireSharedInterruptibly 方法,尝试获取指定数量的共享锁
sync.acquireSharedInterruptibly(permits);
}
acquire() 方法用于获取许可,有两个重载版本,一个用于获取一个许可,另一个用于获取指定数量的许可。这两个方法最终都会调用 Sync 实例的 acquireSharedInterruptibly 方法,该方法在 AQS 中定义:
// AbstractQueuedSynchronizer 中的 acquireSharedInterruptibly 方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 检查当前线程是否被中断,如果是则抛出 InterruptedException 异常
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取共享锁
if (tryAcquireShared(arg) < 0)
// 如果获取失败,则进入等待队列
doAcquireSharedInterruptibly(arg);
}
在 acquireSharedInterruptibly 方法中,首先检查当前线程是否被中断,如果被中断则抛出 InterruptedException 异常。然后调用 tryAcquireShared 方法尝试获取共享锁,该方法在 NonfairSync 和 FairSync 类中分别实现:
非公平模式下的 tryAcquireShared 方法
// NonfairSync 类中的 tryAcquireShared 方法
protected int tryAcquireShared(int acquires) {
// 调用父类的 nonfairTryAcquireShared 方法
return nonfairTryAcquireShared(acquires);
}
// Sync 类中的 nonfairTryAcquireShared 方法
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取当前许可数量
int available = getState();
// 计算剩余许可数量
int remaining = available - acquires;
if (remaining < 0 ||
// 使用 CAS 操作尝试更新许可数量
compareAndSetState(available, remaining))
return remaining;
}
}
在非公平模式下,tryAcquireShared 方法会调用 nonfairTryAcquireShared 方法。该方法通过一个无限循环,不断尝试获取许可。首先获取当前许可数量,然后计算剩余许可数量。如果剩余许可数量小于 0,说明许可数量不足,直接返回剩余许可数量;否则,使用 CAS(Compare And Swap)操作尝试更新许可数量,如果更新成功,则返回剩余许可数量。
公平模式下的 tryAcquireShared 方法
// FairSync 类中的 tryAcquireShared 方法
protected int tryAcquireShared(int acquires) {
for (;;) {
// 检查是否有线程在当前线程之前排队等待
if (hasQueuedPredecessors())
return -1;
// 获取当前许可数量
int available = getState();
// 计算剩余许可数量
int remaining = available - acquires;
if (remaining < 0 ||
// 使用 CAS 操作尝试更新许可数量
compareAndSetState(available, remaining))
return remaining;
}
}
在公平模式下,tryAcquireShared 方法会先检查是否有线程在当前线程之前排队等待,如果有则返回 -1,表示获取失败;否则,与非公平模式一样,尝试获取许可。
如果 tryAcquireShared 方法返回值小于 0,说明获取许可失败,会调用 doAcquireSharedInterruptibly 方法将当前线程加入等待队列并阻塞:
// AbstractQueuedSynchronizer 中的 doAcquireSharedInterruptibly 方法
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 创建一个共享模式的节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在 doAcquireSharedInterruptibly 方法中,首先创建一个共享模式的节点并加入等待队列,然后进入一个无限循环:
- 检查当前节点的前驱节点是否为头节点,如果是头节点则再次尝试获取共享锁(调用
tryAcquireShared方法)。 - 如果获取共享锁成功,调用
setHeadAndPropagate方法将当前节点设置为头节点,并唤醒后续等待的节点。 - 如果获取共享锁失败,调用
shouldParkAfterFailedAcquire方法判断当前线程是否应该阻塞,如果应该阻塞则调用parkAndCheckInterrupt方法将线程阻塞。如果线程在阻塞期间被中断,则抛出InterruptedException异常。
4.3 release() 方法
// 释放一个许可
public void release() {
// 调用同步器的 releaseShared 方法,释放一个共享锁
sync.releaseShared(1);
}
// 释放指定数量的许可
public void release(int permits) {
// 检查传入的许可数量是否小于 0,如果是则抛出异常
if (permits < 0) throw new IllegalArgumentException();
// 调用同步器的 releaseShared 方法,释放指定数量的共享锁
sync.releaseShared(permits);
}
release() 方法用于释放许可,有两个重载版本,一个用于释放一个许可,另一个用于释放指定数量的许可。这两个方法最终都会调用 Sync 实例的 releaseShared 方法,该方法在 AQS 中定义:
// AbstractQueuedSynchronizer 中的 releaseShared 方法
public final boolean releaseShared(int arg) {
// 尝试释放共享锁
if (tryReleaseShared(arg)) {
// 如果释放成功,则唤醒等待队列中的线程
doReleaseShared();
return true;
}
return false;
}
在 releaseShared 方法中,首先调用 tryReleaseShared 方法尝试释放共享锁,该方法在 Sync 类中实现:
// Sync 类中的 tryReleaseShared 方法
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 获取当前许可数量
int current = getState();
// 计算新的许可数量
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
// 使用 CAS 操作尝试更新许可数量
if (compareAndSetState(current, next))
return true;
}
}
tryReleaseShared 方法通过一个无限循环,不断尝试释放许可。首先获取当前许可数量,然后计算新的许可数量。如果新的许可数量小于当前许可数量,说明发生了溢出,抛出错误;否则,使用 CAS 操作尝试更新许可数量,如果更新成功,则返回 true。
如果 tryReleaseShared 方法返回 true,说明释放许可成功,会调用 doReleaseShared 方法唤醒等待队列中的线程:
// AbstractQueuedSynchronizer 中的 doReleaseShared 方法
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, Node.CONDITION))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
在 doReleaseShared 方法中,通过一个无限循环,检查头节点是否存在且不是尾节点。如果头节点的 waitStatus 为 Node.SIGNAL,说明头节点的后继节点需要被唤醒,使用 compareAndSetWaitStatus 方法将头节点的 waitStatus 设置为 Node.CONDITION,然后调用 unparkSuccessor 方法唤醒后继节点。如果头节点的 waitStatus 为 0,尝试将其设置为 Node.PROPAGATE,以确保唤醒操作能够传播到后续节点。
4.4 tryAcquire() 方法
// 尝试获取一个许可
public boolean tryAcquire() {
// 调用同步器的 nonfairTryAcquireShared 方法,尝试获取一个许可
return sync.nonfairTryAcquireShared(1) >= 0;
}
// 尝试获取指定数量的许可
public boolean tryAcquire(int permits) {
// 检查传入的许可数量是否小于 0,如果是则抛出异常
if (permits < 0) throw new IllegalArgumentException();
// 调用同步器的 nonfairTryAcquireShared 方法,尝试获取指定数量的许可
return sync.nonfairTryAcquireShared(permits) >= 0;
}
tryAcquire() 方法用于尝试获取许可,有两个重载版本,一个用于尝试获取一个许可,另一个用于尝试获取指定数量的许可。这两个方法最终都会调用 Sync 实例的 nonfairTryAcquireShared 方法,如果返回值大于等于 0,说明获取许可成功,返回 true;否则返回 false。
4.5 tryAcquire(long timeout, TimeUnit unit) 方法
// 在指定时间内尝试获取一个许可
public boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException {
// 调用同步器的 tryAcquireSharedNanos 方法,在指定时间内尝试获取一个许可
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// 在指定时间内尝试获取指定数量的许可
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
throws InterruptedException {
// 检查传入的许可数量是否小于 0,如果是则抛出异常
if (permits < 0) throw new IllegalArgumentException();
// 调用同步器的 tryAcquireSharedNanos 方法,在指定时间内尝试获取指定数量的许可
return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
}
tryAcquire(long timeout, TimeUnit unit) 方法用于在指定时间内尝试获取许可,有两个重载版本,一个用于在指定时间内尝试获取一个许可,另一个用于在指定时间内尝试获取指定数量的许可。这两个方法最终都会调用 Sync 实例的 tryAcquireSharedNanos 方法,该方法在 AQS 中定义:
// AbstractQueuedSynchronizer 中的 tryAcquireSharedNanos 方法
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 检查当前线程是否被中断,如果是则抛出 InterruptedException 异常
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取共享锁
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
在 tryAcquireSharedNanos 方法中,首先检查当前线程是否被中断,如果被中断则抛出 InterruptedException 异常。然后调用 tryAcquireShared 方法尝试获取共享锁,如果获取成功则返回 true;否则,调用 doAcquireSharedNanos 方法在指定时间内尝试获取共享锁:
// AbstractQueuedSynchronizer 中的 doAcquireSharedNanos 方法
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return true;
}
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在 doAcquireSharedNanos 方法中,首先计算截止时间,然后创建一个共享模式的节点并加入等待队列。接着进入一个无限循环:
- 检查当前节点的前驱节点是否为头节点,如果是头节点则再次尝试获取共享锁(调用
tryAcquireShared方法)。 - 如果获取共享锁成功,调用
setHeadAndPropagate方法将当前节点设置为头节点,并唤醒后续等待的节点,返回true。 - 计算剩余时间,如果剩余时间小于等于 0,说明超时,返回
false。 - 如果需要阻塞且剩余时间大于
spinForTimeoutThreshold,则使用LockSupport.parkNanos方法将线程阻塞指定的时间。 - 如果线程在阻塞期间被中断,则抛出
InterruptedException异常。
五、Semaphore 的使用场景分析
5.1 限制并发访问资源
在某些情况下,我们需要限制同时访问某个资源的线程数量,以避免资源竞争和数据不一致等问题。例如,一个数据库连接池中的连接数量是有限的,我们可以使用 Semaphore 来控制同时获取数据库连接的线程数量。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.Semaphore;
public class DatabaseConnectionPool {
// 定义最大连接数
private static final int MAX_CONNECTIONS = 5;
// 创建一个 Semaphore 实例,初始许可数量为最大连接数
private static final Semaphore semaphore = new Semaphore(MAX_CONNECTIONS);
// 获取数据库连接
public static Connection getConnection() throws InterruptedException, SQLException {
// 线程尝试获取一个许可
semaphore.acquire();
try {
// 模拟获取数据库连接
return DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "username", "password");
} catch (SQLException e) {
// 如果获取连接失败,释放许可
semaphore.release();
throw e;
}
}
// 释放数据库连接
public static void releaseConnection(Connection connection) {
if (connection != null) {
try {
// 关闭数据库连接
connection.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 释放许可
semaphore.release();
}
}
}
}
在这个示例中,我们创建了一个 Semaphore 实例,初始许可数量为最大连接数。在获取数据库连接时,线程需要先获取一个许可,如果许可数量不足,线程会被阻塞;在释放数据库连接时,线程需要释放一个许可。这样就可以确保同时访问数据库连接池的线程数量不会超过最大连接数。
5.2 实现生产者 - 消费者模型
Semaphore 还可以用于实现生产者 - 消费者模型,通过控制缓冲区的可用空间和已占用空间,来协调生产者和消费者线程的执行。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Semaphore;
public class ProducerConsumerExample {
// 定义缓冲区的最大容量
private static final int BUFFER_SIZE = 5;
// 创建一个 Semaphore 实例,初始许可数量为缓冲区的最大容量,表示可用空间
private static final Semaphore availableSpace = new Semaphore(BUFFER_SIZE);
// 创建一个 Semaphore 实例,初始许可数量为 0,表示已占用空间
private static final Semaphore occupiedSpace = new Semaphore(0);
// 定义一个队列作为缓冲区
private static final Queue<Integer> buffer = new LinkedList<>();
public static void main(String[] args) {
// 创建并启动生产者线程
Thread producer = new Thread(new Producer());
// 创建并启动消费者线程
Thread consumer = new Thread(new Consumer());
producer.start();
consumer.start();
}
static class Producer implements Runnable {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
// 线程尝试获取一个可用空间的许可
availableSpace.acquire();
synchronized (buffer) {
// 向缓冲区添加元素
buffer.add(i);
System.out.println("生产者生产了元素:" + i);
}
// 释放一个已占用空间的许可
occupiedSpace.release();
// 模拟生产时间
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
// 线程尝试获取一个已占用空间的许可
occupiedSpace.acquire();
synchronized (buffer) {
// 从缓冲区取出元素
int item = buffer.poll();
System.out.println("消费者消费了元素:" + item);
}
// 释放一个可用空间的许可
availableSpace.release();
// 模拟消费时间
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在这个示例中,我们使用两个 Semaphore 实例来控制缓冲区的可用空间和已占用空间。生产者线程在生产元素之前需要先获取一个可用空间的许可,生产完成后释放一个已占用空间的许可;消费者线程在消费元素之前需要先获取一个已占用空间的许可,消费完成后释放一个可用空间的许可。这样就可以确保生产者不会在缓冲区满时继续生产,消费者不会在缓冲区空时继续消费。
六、Semaphore 的异常处理
6.1 InterruptedException
当线程在等待许可的过程中被中断时,会抛出 InterruptedException 异常。例如,在调用 acquire() 或 tryAcquire(long timeout, TimeUnit unit) 方法时,如果线程被中断,会抛出该异常。
import java.util.concurrent.Semaphore;
public class InterruptedExceptionExample {
private static final Semaphore semaphore = new Semaphore(1);
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
System.out.println("线程正在等待许可...");
// 线程尝试获取一个许可
semaphore.acquire();
System.out.println("线程已获取许可,开始执行任务...");
// 模拟任务执行时间
Thread.sleep(2000);
System.out.println("线程任务执行完毕,释放许可");
// 线程释放一个许可
semaphore.release();
} catch (InterruptedException e) {
System.out.println("线程被中断:" + e.getMessage());
}
});
thread.start();
try {
// 主线程休眠 1 秒后中断子线程
Thread.sleep(1000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,子线程在等待许可的过程中被主线程中断,会抛出 InterruptedException 异常。
6.2 IllegalArgumentException
当调用 acquire(int permits)、release(int permits)、tryAcquire(int permits) 或 tryAcquire(int permits, long timeout, TimeUnit unit) 方法时,如果传入的许可数量小于 0,会抛出 IllegalArgumentException 异常。
import java.util.concurrent.Semaphore;
public class IllegalArgumentExceptionExample {
private static final Semaphore semaphore = new Semaphore(1);
public static void main(String[] args) {
try {
// 尝试获取负数个许可,会抛出 IllegalArgumentException 异常
semaphore.acquire(-1);
} catch (IllegalArgumentException | InterruptedException e) {
System.out.println("异常:" + e.getClass().getSimpleName() + " - " + e.getMessage());
}
}
七、Semaphore 公平模式与非公平模式的详细对比
7.1 公平模式原理
公平模式的核心目标是保证线程按照请求许可的顺序依次获取许可。在 Java 的 Semaphore 中,公平模式通过 FairSync 类来实现。
7.1.1 公平获取许可的源码分析
// FairSync 类中的 tryAcquireShared 方法
protected int tryAcquireShared(int acquires) {
for (;;) {
// 检查是否有线程在当前线程之前排队等待
if (hasQueuedPredecessors())
return -1;
// 获取当前许可数量
int available = getState();
// 计算剩余许可数量
int remaining = available - acquires;
if (remaining < 0 ||
// 使用 CAS 操作尝试更新许可数量
compareAndSetState(available, remaining))
return remaining;
}
}
在 tryAcquireShared 方法中,首先会调用 hasQueuedPredecessors() 方法检查是否有线程在当前线程之前排队等待。hasQueuedPredecessors() 方法的源码如下:
// AbstractQueuedSynchronizer 中的 hasQueuedPredecessors 方法
public final boolean hasQueuedPredecessors() {
Node t = tail; // 尾节点
Node h = head; // 头节点
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这个方法会判断等待队列是否存在元素(即 h != t),如果存在元素,会进一步检查头节点的下一个节点是否为空或者该节点对应的线程是否为当前线程。如果存在其他线程在当前线程之前排队,hasQueuedPredecessors() 方法会返回 true,此时 tryAcquireShared 方法会直接返回 -1,表示获取许可失败。只有当没有其他线程在当前线程之前排队时,才会尝试获取许可。
7.1.2 公平模式的优缺点
- 优点:公平模式可以避免线程饥饿问题,即每个线程都有机会按照请求的顺序获取许可,保证了线程获取许可的公平性。这在一些对公平性要求较高的场景中非常有用,例如多个线程按照顺序依次处理任务的场景。
- 缺点:公平模式的性能相对较低。因为每次获取许可时都需要检查等待队列,增加了额外的开销。而且在高并发场景下,频繁的队列检查会导致线程上下文切换频繁,影响系统的整体性能。
7.2 非公平模式原理
非公平模式下,线程在获取许可时不会考虑等待队列中是否有其他线程在等待,只要许可数量足够,就会尝试获取许可。在 Java 的 Semaphore 中,非公平模式通过 NonfairSync 类来实现。
7.2.1 非公平获取许可的源码分析
// NonfairSync 类中的 tryAcquireShared 方法
protected int tryAcquireShared(int acquires) {
// 调用父类的 nonfairTryAcquireShared 方法
return nonfairTryAcquireShared(acquires);
}
// Sync 类中的 nonfairTryAcquireShared 方法
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取当前许可数量
int available = getState();
// 计算剩余许可数量
int remaining = available - acquires;
if (remaining < 0 ||
// 使用 CAS 操作尝试更新许可数量
compareAndSetState(available, remaining))
return remaining;
}
}
在非公平模式下,tryAcquireShared 方法直接调用 nonfairTryAcquireShared 方法。该方法不会检查等待队列,而是直接尝试获取许可。只要当前许可数量足够,就会使用 CAS 操作更新许可数量。
7.2.2 非公平模式的优缺点
- 优点:非公平模式的性能相对较高。因为不需要检查等待队列,减少了额外的开销,避免了线程上下文切换的频繁发生。在高并发场景下,非公平模式可以更快地响应线程的请求,提高系统的吞吐量。
- 缺点:非公平模式可能会导致线程饥饿问题。某些线程可能会因为其他线程频繁获取许可而长时间无法获取许可,从而影响系统的公平性。
7.3 公平模式与非公平模式的性能测试
为了更直观地比较公平模式和非公平模式的性能差异,我们可以进行一个简单的性能测试。
import java.util.concurrent.Semaphore;
public class SemaphoreFairnessPerformanceTest {
private static final int THREAD_COUNT = 100;
private static final int PERMITS = 10;
private static final int ITERATIONS = 1000;
public static void main(String[] args) {
// 测试非公平模式
testSemaphore(false);
// 测试公平模式
testSemaphore(true);
}
private static void testSemaphore(boolean fair) {
Semaphore semaphore = new Semaphore(PERMITS, fair);
Thread[] threads = new Thread[THREAD_COUNT];
long startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
try {
semaphore.acquire();
// 模拟一些操作
Thread.sleep(1);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println((fair ? "公平模式" : "非公平模式") + " 耗时:" + (endTime - startTime) + " 毫秒");
}
}
在这个测试中,我们创建了 100 个线程,每个线程会尝试获取和释放许可 1000 次。通过比较公平模式和非公平模式下的执行时间,可以看出两种模式的性能差异。一般来说,在高并发场景下,非公平模式的执行时间会更短。
八、Semaphore 与其他并发工具的协同使用
8.1 与 CountDownLatch 协同使用
CountDownLatch 可以用于让一个或多个线程等待其他线程完成一组操作,而 Semaphore 可以用于控制同时访问特定资源的线程数量。我们可以将它们结合使用,实现更复杂的并发控制。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
public class SemaphoreAndCountDownLatchExample {
private static final int THREAD_COUNT = 5;
private static final Semaphore semaphore = new Semaphore(2);
private static final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(new Worker(i)).start();
}
try {
// 主线程等待所有子线程完成任务
countDownLatch.await();
System.out.println("所有任务已完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class Worker implements Runnable {
private final int id;
public Worker(int id) {
this.id = id;
}
@Override
public void run() {
try {
// 线程尝试获取一个许可
semaphore.acquire();
System.out.println("线程 " + id + " 已获取许可,开始执行任务...");
// 模拟任务执行时间
Thread.sleep(2000);
System.out.println("线程 " + id + " 任务执行完毕,释放许可");
// 线程释放一个许可
semaphore.release();
// 计数器减 1
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在这个示例中,Semaphore 用于控制同时执行任务的线程数量,而 CountDownLatch 用于让主线程等待所有子线程完成任务。每个子线程在执行任务前会尝试获取一个许可,执行完任务后释放许可,并将 CountDownLatch 的计数器减 1。主线程调用 countDownLatch.await() 方法等待计数器减为 0,即所有子线程都完成任务。
8.2 与 CyclicBarrier 协同使用
CyclicBarrier 允许一组线程相互等待,直到所有线程都到达某个公共屏障点,然后这些线程可以继续执行后续的操作。我们可以将 Semaphore 和 CyclicBarrier 结合使用,实现更复杂的线程同步和资源控制。
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Semaphore;
public class SemaphoreAndCyclicBarrierExample {
private static final int THREAD_COUNT = 5;
private static final Semaphore semaphore = new Semaphore(2);
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(THREAD_COUNT, () -> {
System.out.println("所有线程都已到达屏障点,继续执行后续操作...");
});
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(new Worker(i)).start();
}
}
static class Worker implements Runnable {
private final int id;
public Worker(int id) {
this.id = id;
}
@Override
public void run() {
try {
// 线程尝试获取一个许可
semaphore.acquire();
System.out.println("线程 " + id + " 已获取许可,开始执行任务...");
// 模拟任务执行时间
Thread.sleep(2000);
System.out.println("线程 " + id + " 任务执行完毕,等待其他线程到达屏障点");
// 线程释放一个许可
semaphore.release();
// 等待其他线程到达屏障点
cyclicBarrier.await();
System.out.println("线程 " + id + " 继续执行后续操作");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
在这个示例中,Semaphore 用于控制同时执行任务的线程数量,而 CyclicBarrier 用于让所有线程在完成任务后相互等待,直到所有线程都到达屏障点。每个子线程在执行任务前会尝试获取一个许可,执行完任务后释放许可,并调用 cyclicBarrier.await() 方法等待其他线程到达屏障点。当所有线程都到达屏障点后,会执行 CyclicBarrier 中指定的任务,然后所有线程继续执行后续操作。
九、Semaphore 的性能优化与注意事项
9.1 性能优化策略
9.1.1 合理设置许可数量
许可数量的设置直接影响 Semaphore 的性能和系统的并发能力。如果许可数量设置得过大,可能会导致资源过度竞争,降低系统的性能;如果许可数量设置得过小,可能会限制系统的并发能力,导致部分线程长时间等待。因此,需要根据系统的实际情况和资源限制,合理设置许可数量。
9.1.2 选择合适的公平模式
在高并发场景下,非公平模式通常具有更好的性能。因为非公平模式不需要检查等待队列,减少了额外的开销,避免了线程上下文切换的频繁发生。但是,如果对公平性有较高的要求,例如需要保证每个线程都有机会按照请求的顺序获取许可,那么应该选择公平模式。
9.1.3 减少锁的持有时间
在使用 Semaphore 时,尽量减少线程持有许可的时间。例如,在获取许可后,尽快完成必要的操作,然后释放许可。这样可以提高许可的利用率,减少其他线程的等待时间。
9.2 注意事项
9.2.1 异常处理
在使用 Semaphore 时,需要注意异常处理。例如,在调用 acquire() 或 tryAcquire(long timeout, TimeUnit unit) 方法时,可能会抛出 InterruptedException 异常,需要进行适当的处理。另外,在调用 acquire(int permits)、release(int permits)、tryAcquire(int permits) 或 tryAcquire(int permits, long timeout, TimeUnit unit) 方法时,如果传入的许可数量小于 0,会抛出 IllegalArgumentException 异常,需要进行参数检查。
9.2.2 避免死锁
在使用 Semaphore 时,需要注意避免死锁的发生。例如,在多个线程同时获取多个 Semaphore 实例的许可时,如果获取许可的顺序不一致,可能会导致死锁。为了避免死锁,可以采用固定的许可获取顺序,或者使用超时机制。
9.2.3 资源泄漏
在使用 Semaphore 时,需要确保每个线程在使用完许可后都能正确释放许可,避免资源泄漏。例如,在捕获到异常时,也需要在 finally 块中释放许可。
十、总结与展望
10.1 总结
通过对 Java Semaphore 的源码分析和使用场景介绍,我们可以总结出以下几点:
- 功能强大:
Semaphore是一个非常实用的并发工具,它可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理地使用公共资源。 - 实现原理清晰:
Semaphore基于AbstractQueuedSynchronizer(AQS)实现,通过维护一个许可的计数来控制线程的访问。它提供了公平模式和非公平模式,开发者可以根据实际需求进行选择。 - 使用场景广泛:
Semaphore在限制并发访问资源、实现生产者 - 消费者模型等场景中有着广泛的应用,可以提高程序的并发性能和效率。 - 异常处理完善:
Semaphore提供了完善的异常处理机制,当线程在等待许可的过程中被中断或传入的参数不合法时,会抛出相应的异常,开发者可以根据异常信息进行相应的处理。
10.2 展望
随着 Java 技术的不断发展和应用场景的不断扩展,Semaphore 可能会在以下几个方面得到进一步的优化和改进:
- 性能优化:在高并发场景下,
Semaphore的性能可能会受到一定的影响。未来可以通过优化锁的使用、减少线程上下文切换等方式来提高其性能。 - 功能扩展:可以考虑为
Semaphore增加一些新的功能,如支持动态调整许可数量、支持不同类型的许可等,以满足更多的应用场景需求。 - 与其他并发工具的集成:可以将
Semaphore与其他并发工具(如ExecutorService、CompletableFuture等)进行更深入的集成,提供更强大的并发编程能力。
总之,Semaphore 作为 Java 并发编程中的一个重要工具,在未来的发展中有着广阔的前景。通过不断的优化和改进,它将为开发者提供更加高效、灵活的同步解决方案。