目录
- Semaphore 简介
- Semaphore 使用示例
- Semaphore 实现原理
- Semaphore 源码解析
Semaphore 简介
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
使用示例
在一个需求,开启 30 个线程并发读取文件,并存储至数据库,但数据库只有 10 个连接,如果 30 个线程同时存库,则会导致无法获取数据库连接,这时必须控制只有 10 个线程同时获取数据库连接进行数据保存。
public class Main {
private static final int THREAD_COUNT = 30;
private static final int CONNECTION_COUNT = 10;
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(CONNECTION_COUNT);
IntStream.range(0, THREAD_COUNT).forEach(i -> new Thread(() -> {
try {
semaphore.acquire();
System.out.println("Thread["+i+"] execute save");
Thread.sleep(1000L);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start());
}
}
虽然有 30 个线程在运行,但是只允许 10 个并发执行,Semaphore 在构建时传入初始值,表示可用的许可证数量,线程调用 acquire 方法可以获取一个许可证,当 10 个线程调用 acquire 方法,许可证被领完,后续线程则会阻塞,领取许可证的线程执行 release 方法归还许可证,此时被阻塞的线程就可以继续获取许可证。
Semaphore 还提供了一些其它方法
- int availablePermits():返回信号量中当前可用的许可证数量
- int getQueueLength():返回正在等待获取许可证的线程数
- boolean hasQueuedThreads():是否有线程正在等待获取许可证
- void reducePermits(int reduction):减少 reduction 个许可证
- Collection getQueuedThreads():返回所有等待获取许可证的线程集合
实现原理
Semaphore 内部是通过 AQS 同步器来实现的,多个线程同时运行,可以使用 AQS 的共享锁机制,控制并发线程数量,可以通过共享状态值进行控制。
- 第一步:初始化可颁发许可证数量,后台处理为:创建 Semaphore 对象,将 AQS 同步状态设置为可颁发许可证数量。
- 第二步:颁发许可证,多个线程调用 acquire() 方法领取许可证,后台处理为:执行加共享锁,每次加锁同步状态减 1,如果同步状态大于等于 0,加锁成功,即成功颁发许可证,如果同步状态小于 0,则加锁失败,线程进入 AQS 等待队列阻塞等待,所以同步状态最小为 0。
- 第三步:归还许可证,获取许可证的线程执行完逻辑后需要调用 release() 方法归还许可证,后台处理为:执行释放共享锁,释放成功,同步状态加 1,同时会唤醒 AQS 等待队列中的队首线程。
Semaphore 提供了公平锁与非公平锁两种模式,所谓公平与非公平指的是领取许可证时,需要判断等待队列中是否有线程在等待,如果有线程在等待,那么公平锁会直接进入等待队列队尾,而非公平锁则与等待队列中的线程竞争这个许可证。通俗点讲就是,有一队人在排队挂号,非公平锁机制就是,先去窗口与第一个人挤一下,看能不能插个队,如果插不上队,就去后面排队,而公平锁机制就是老实人机制,直接去后面排队。虽然生活中做个老实人比较好一点,但在计算机中,更鼓励不要太老实。
公平锁与非公平锁的机制 ReentrantLock
也有提供,可以参考本系列的 JUC源码系列之ReentrantLock源码解析
源码解析
通过上面的实现原理分析,已经大概了解了基本实现思路,以非公平锁为例,看一下内部实现代码。
首先查看构造方法,Semaphore 有两个构造方法,permits
参数用于设置可颁发许可证数量,fair
参数用于设置内部使用公平锁还是非公平锁,默认使用非公平锁。
public class Semaphore implements java.io.Serializable {
private final Sync sync;
// 默认创建非公平锁
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
// 根据入参决定创建公平锁还是非公平锁
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
}
以非公平锁为例,继续查看源码,公平锁与非公平锁的差别无非是加锁时的操作有点差异,在文章后面会解释其间差异。
非公平锁的实现非常简单,只有两个方法,构造方法直接调用父类的构造方法,tryAcquireShared
方法执行加锁时会被调用,也是执行父类中的已有实现,所以主体实现其实都在 Sync
类中。
static final class NonfairSync extends Sync {
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
Sync
类继承了 AQS 同步器,现在看的是构造方法,构造方法直接调用 AQS 提供的 setState
方法,设置同步状态的值,将其设置为构造方法传入的可颁发许可证数量。
abstract static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) {
setState(permits);
}
//... 省略其它内容
}
到此处,可以知晓 Semaphore 内部实现是委托 AQS 同步器(Sync)实现,构造方法完成两件事
- 确定同步器实现,公平锁(FairSync)与非公平锁(NonfairSync)
- 初始化同步状态
接着查看颁发许可证的源码实现,acquire
方法其实提供了很多个,原理都是一样的,这里列举出几个。可以看出方法体都是一行代码,意思就是委托给 AQS 同步器。
// 方法1,阻塞式加锁,可响应中断
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 方法2,阻塞式加锁,不可响应中断
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
// 方法3,快速失败式加锁
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
上面的方法1、方法2调用的是 AQS 原始提供的加锁方法,方法3调用的是 AQS 实现类 Sync
自实现的方法。AQS 提供的加锁方法最终还是会调用 Sync
自实现的方法执行加锁,只是 AQS 还提供了一套完整的等待队列机制,就是说加锁不成功就会进入等待队列阻塞式等待,所以具体能否加锁成功还是得看 Sync
是怎么实现的。
非公平锁的加锁逻辑也比较简单,就是通过死循环不断地使用 cas 操作同步状态,操作成功则加锁成功,直到同步状态小于 0,加锁失败,此时方法3返回加锁失败,由线程自行决定如何操作,而执行方法1、2的线程则会自动进入 AQS 等待队列。
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState(); // 获取同步状态值
int remaining = available - acquires; // 减掉 acquires
if (remaining < 0 || // 减掉后必须大于等于 0
compareAndSetState(available, remaining)) // cas 同步状态
return remaining;
}
}
最后查看归还许可证的源码实现,直接调用了 AQS 的 releaseShared
方法,这个方法会调用 tryReleaseShared
方法执行归还操作。
public void release() {
sync.releaseShared(1);
}
// Sync 类的方法
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState(); // 获取同步状态值
int next = current + releases; // 加上 releases
if (next < current) // 处理 int 越界异常
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next)) // cas 同步状态
return true;
}
}
AQS 本身提供了一整套的阻塞等待队列机制,加锁机制只需要通过控制同步状态即可,所以从代码实现来看是非常简单的。
前文中提到了公平锁与非公平锁的区别在于加锁时是否加入竞争,所以公平锁加锁时首先调用 hasQueuedPredecessors()
方法判断等待队列是否为空,如果不为空,则加锁失败,线程自动进入 AQS 等待队列。hasQueuedPredecessors()
方法也是 AQS 提供的方法,可以看出 AQS 已经把所有的事情都做完了。
static final class FairSync extends Sync {
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;
}
}
}