Semaphore
Semaphore 是信号量的意思,一般用来控制同时访问某个资源的线程数量,协调各个线程合理的访问公共资源,Semaphore 的底层依赖的是 AQS。
Semaphore 使用计数器来控制对共享资源的访问, 如果计数器大于0,则表示允许访问共享资源, 如果为 0,则表示共享资源已经达到访问的上限就拒绝访问, 计数器的计数的就是允许同时访问共享资源的线程数。
知识储备传送门:
深入理解 AbstractQueuedSynchronizer(AQS)【源码分析】
深入理解 ReentrantLock 【源码分析】
CAS的使用以及底层原理详解
深入理解 CountDownLatch 【源码分析】
Semaphore 的应用场景:
控制并发量,通常用于那些资源有明确访问数量限制的场景,常用于限流 ,比如停车场场景,停车场内部车位有限,同时只能停有限车辆,比如窗口办业务场景,窗口有限,同时只能有限的人同时办业务。
Semaphore 使用简单举例:
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int a = 0; a < 4; a++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println("当前线程获取许可,线程名称:" + Thread.currentThread().getName() + ";当前时间:" + System.currentTimeMillis() / 1000);
Thread.sleep(2000);
semaphore.release();
System.out.println("当前线程释放许可,线程名称:" + Thread.currentThread().getName() + ";当前时间:" + System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
}).start();
}
}
执行结果:
当前线程获取许可,线程名称:Thread-0;当前时间:1712322271
当前线程获取许可,线程名称:Thread-1;当前时间:1712322271
当前线程释放许可,线程名称:Thread-0;当前时间:1712322273
当前线程获取许可,线程名称:Thread-3;当前时间:1712322273
当前线程获取许可,线程名称:Thread-2;当前时间:1712322273
当前线程释放许可,线程名称:Thread-1;当前时间:1712322273
当前线程释放许可,线程名称:Thread-2;当前时间:1712322275
当前线程释放许可,线程名称:Thread-3;当前时间:1712322275
执行结果分析:
我们的案例代码是想同时启动 4 个线程,Semaphore 只有两个信号量,我们发现 1712322271 时候,有两个线程获取了许可,执行了业务代码,1712322273 时刻有两个线程释放了许可,同时另外两个线程才获取到许可,开始执行业务代码,达到了使用 Semaphore 信号量控制并发的目的。
Semaphore 常用方法:
- acquire() :获取一个执行许可,在获取到执行许可之前或者被其他线程中断之前,线程一直会被阻塞。
- acquire(int permits) :获取 permits 个执行许可,在获取到执行许可之前或者被其他线程中断之前,线程一直会被阻塞。
- acquireUninterruptibly() :获取一个执行许可,在获取到执行许可之前(忽略中断),线程一直会被阻塞。
- tryAcquire() :尝试获取一个执行许可,返回获取的结果,线程不会被阻塞。
- release() :释放一个执行许可,同时唤醒一个在阻塞中小获取执行许可的线程。
- hasQueuedThreads () :队列中是否还存在等待的线程。
- getQueueLength() :获取等待队列中的线程数
- drainPermits() :清空所有执行许可,返回清空了多少个执行许可。
- availablePermits() :获取有效的执行许可数量。
Semaphore 类结构:
类结构分析:
Semaphore 类内部总共存在 Sync、NonfairSync、FairSync 三个类,NonfairSync(非公平锁) 与 FairSync(公平锁) 类都继承 Sync 类,Sync 类继承自 AbstractQueuedSynchronizer 抽象类,这跟我们前面分析的 ReentrantLock 很像。
Semaphore 源码分析
Semaphore#acquire 方法源码分析:
//从信号量中获取许可,阻塞直到有一个信号量可用,或者线程被中断
public void acquire() throws InterruptedException {
//这里本质是调用 AQS#acquireSharedInterruptibly 方法
sync.acquireSharedInterruptibly(1);
}
//以共享的模式获取信号量
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//检查线程中断状态 如果中断则中止
if (Thread.interrupted())
throw new InterruptedException();
//以共享的模式去获取信号量 返回小于0 表示获取信号量失败
if (tryAcquireShared(arg) < 0)
//进入阻塞队列
doAcquireSharedInterruptibly(arg);
}
acquire 方法逻辑比较简单,就是一些简单的判断和对一些方法的封装,我们接下来分析一下 tryAcquireShared、 doAcquireSharedInterruptibly 方法。
NonfairSync#tryAcquireShared 源码分析:
AQS 把 tryAcquireShared 方法交给了子类重写,因此 tryAcquireShared 方法实际上调用的是 NonfairSync 或者 FairSync 的 tryAcquireShared 方法,这里我们分析 NonfairSync 的 tryAcquireShared 方法,因为 Semaphore 的默认实现也是 Semaphore。
//以非公平的方式获取信号量许可 只是一个方法封装
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
//以非公平的方式获取信号量许可
final int nonfairTryAcquireShared(int acquires) {
//自旋 保证一定能够获取到剩余的信号量许可 有可能返回负数
for (;;) {
//获取可用的信号量 getState 获取 volatile 修饰的变量 state
int available = getState();
//用可用的信号量减去想要获取的信号量就是剩余的信号量
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
//1.如果剩余的信号量小于 0
//2.设置信号量为刚刚减法得到的信号量 并使用 CAS 设置给 state
//返回获取到的信号量 结束自旋
return remaining;
}
}
AQS#doAcquireSharedInterruptibly 方法源码分析:
//就是将当前线程加入等待队列
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//将当先线程封装成一个 node 节点
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) {
//剩余信号量大于等于0 表示获取信号量许可成功 设置头节点并传播信号量
setHeadAndPropagate(node, r);
//把刚刚的头结点的后驱节点设置为 null 方法JVM GC
p.next = null; // help GC
failed = false;
return;
}
}
//根据当前节点的前驱节点 判断当前节点是否应该被阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
//设置节点状态为取消节点 程序异常才会走到这里
cancelAcquire(node);
}
}
//设置头节点并传播信号量
private void setHeadAndPropagate(Node node, int propagate) {
//获取头结点
Node h = head; // Record old head for check below
//设置当前节点为头节点
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//1.剩余信号量大于0
//2.头结点不为空
//3.头接待的状态为 SIGNAL
//4.头节点就是刚刚设置的节点 且为空
//5.头接待的状态为 SIGNAL
//SIGNAL 状态 则其后驱节点等待被唤醒 也可以理解为当前节点将要释放锁 需要唤醒后续节点
//获取当前节点的后驱节点
Node s = node.next;
//当前节点的后驱节点不为空 且也是共享模式
if (s == null || s.isShared())
//释放信号量许可
doReleaseShared();
}
}
//根据当前节点的前驱节点 判断当前节点是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//当前节点的前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 根据 Node 源码中的状态解析 我们知道当前节点的前驱节点处于唤醒状态
return true;
if (ws > 0) {
// 根据 Node 源码中的状态解析 我们知道 前驱节点是取消状态 我们需要在队列中移除它 并循环移除它前面的节点 并找到有效的前驱节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//当前节点的前驱节点既不是唤醒状态 也不是取消节点 那就 CAS 设置当前节点的前驱节点状态为 Node.SIGNAL 阻塞状态
//此时前驱节点的状态 只能是0 或者是 PROPAGATE -3
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//不阻塞
return false;
}
//设置节点状态为取消节点
private void cancelAcquire(Node node) {
//为空判断
if (node == null)
return;
//设置当前节点的所属线程为空
node.thread = null;
//获取当前节点的前驱节点
Node pred = node.prev;
//前驱节点状态大于0 表示取消节点 一直找到有效的前驱节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//获取当前节点的 前驱节点的 后驱节点
Node predNext = pred.next;
//设置当前节点的状态为 取消状态
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
//如果当前节点是尾节点 且 CAS 操作将当前节点的前驱节点设置为 尾节点成功 则 CAS 操作将当前节点的前驱节点的后驱节点设置为 null
compareAndSetNext(pred, predNext, null);
} else {
//如果当前节点是尾节点 且 CAS 操作将当前节点的前驱节点设置为 尾节点失败
//节点状态
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//当前节点的前驱节点不是头节点
//1 当前节点的前驱节点状态为阻塞
//2 当前节点的前驱节点状态小于0 且将前驱节点的状态设置为 Node.SIGNAL
//1 2 有一个为 true 在判断当前节点的前驱节点线程是否为空
//获取当前节点的后驱节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
//当前节点的后驱节点不为空 且状态小于0
//CAS 操作把当前节点的前驱节点的后驱节点设置为当前节点的后驱节点
compareAndSetNext(pred, predNext, next);
} else {
//如果当前节点就是head 节点的后驱节点 或者不满足上述条件 就唤醒当前节点的后面的节点
unparkSuccessor(node);
}
//被设置为取消的节点的后驱节点指向自己 方便JVM内存回收
node.next = node; // help GC
}
}
doAcquireSharedInterruptibly 方法就是把获取信号量许可失败的线程加入到阻塞队列中,在这个过程中主要由两个步骤,如下:
- 将获取信号量许可失败的线程包装为 Node 节点添加到阻塞队列中。
- 判断当前节点是否是头结点的后驱节点,如果是则再次尝试获取信号量许可,获取成功传播信号量使用情况并返回,否则则进行睡眠等待唤醒。
Semaphore#release 方法源码分析:
Semaphore#release 方法的作用就是释放信量许可,并唤醒后续节点的线程去获取信号量。
//释放信号量许可
public void release() {
//调用AQS 的releaseShared方法
sync.releaseShared(1);
}
//释放信号量许可
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//释放信号量许可 Semaphore#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");
if (compareAndSetState(current, next))
// CAS 把上面计算出来的信号量设置给信号量许可 返回释放成功
return true;
}
}
//共享模式的释放操作 向后继者发出信号并确保传播
private void doReleaseShared() {
//自旋
for (;;) {
//获取头节点
Node h = head;
//头结点不为空 且头结点是尾节点 表示有等待获取许可的节点
if (h != null && h != tail) {
//获取头节点的等待状态
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//节点为 SIGNAL 状态 则其后驱节点等待被唤醒 也可以理解为当前节点将要释放锁 需要唤醒后续节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//CAS 设置头节点的状态为0 0表示初始化节点
continue; // loop to recheck cases
//唤醒头结点的后续节点
unparkSuccessor(h);
}
//如果头结点的等待状态为 0 就设置等待状态为 PROPAGATE -3 表示要唤醒头节点后驱节点的后驱节点
//这种情况发生在线程A 刚释放完线程许可通过 CAS compareAndSetWaitStatus(h, Node.SIGNAL, 0)
//此时线程B 也要释放许可 发现等待状态为 0 此时线程B 执行 CAS compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
//这样以来就有释放了两个信号量许可 因此唤醒头节点后驱节点的后驱节点
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
// 并发情况下,可能会出现wa为0,需要状态为PROPAGATE,保证唤
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
Semaphore#release 释放信号量方法总结:
- 释放信号量许可,使用 CAS 更新信号量许可值。
- 唤醒阻塞队列中的有效节点(因为并发问题,引入了 PROPAGATE 状态),让节点所在的线程继续去争抢信号量许可。
Semaphore 总结:
- 熟悉 Semaphore 信号量许可的获取和释放流程,其实就是共享锁的获取和释放流程。
- 理解 Semaphore 的底层原理,其实是使用了 AQS 的共享锁模式。
如有错误的地方欢迎指出纠正。