简单介绍
信号量,这个类的作用有点类似于“许可证”。有时,我们因为一些原因需要控制同时访问共享资源的最大线程数量,比如出于系统性能的考虑需要限流,或者共享资源是稀缺资源,我们需要有一种办法能够协调各个线程,以保证合理的使用公共资源。
Semaphore维护了一个许可集,其实就是一定数量的“许可证”。当有线程想要访问共享资源时,需要先获取(acquire)的许可;如果许可不够了,线程需要一直等待,直到许可可用。当线程使用完共享资源后,可以归还(release)许可,以供其它需要的线程使用。
另外,Semaphore支持公平/非公平策略,这和ReentrantLock类似,后面讲Semaphore原理时会看到,它们的实现本身就是类似的。
简单使用
package com.liaoxiang.multithreading3.middle.aqs_lock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* 信号量-Semaphore
* 主要方法:
* semaphore.acquire()
* semaphore.release()
*/
public class SemaphoreTest {
public static void main(String[] args) {
// thread_pool
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5个线程同时访问
final Semaphore semaphore = new Semaphore(5);
// 模拟20个客户端访问
for (int index = 1; index <= 10; index++) {
final int NO = index;
exec.execute(() -> {
try {
// 获取许可
semaphore.acquire();
System.out.println("业务执行中: " + NO + "...");
//模拟实际业务逻辑
Thread.sleep((long) (Math.random() * 5000));
// 释放许可
semaphore.release();
System.out.println("执行完成了: " + NO);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 退出线程池
exec.shutdown();
}
}
执行效果:首先有五个线程同时执行任务,如果有一个线程执行完成,立马加入一个新的线程继续执行,直到所有线程执行完成
原理分析
熟悉的味道。。。Semaphore果然是通过内部类实现了AQS框架提供的接口,而且基本结构几乎和ReentrantLock完全一样,通过两个内部类分别实现了公平/非公平策略。
构造器
创建一个给定许可数量的信号量对象,默认使用非公平锁,通过第二个参数可设置为公平锁
/**
* Creates a {@code Semaphore} with the given number of
* permits and nonfair fairness setting.
*
* @param permits the initial number of permits available.
* This value may be negative, in which case releases
* must occur before any acquires will be granted.
*/
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
/**
* Creates a {@code Semaphore} with the given number of
* permits and the given fairness setting.
*
* @param permits the initial number of permits available.
* This value may be negative, in which case releases
* must occur before any acquires will be granted.
* @param fair {@code true} if this semaphore will guarantee
* first-in first-out granting of permits under contention,
* else {@code false}
*/
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
主要方法
- 1、Semaphore#acquire()
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 调用父类AQS中的获取共享锁资源的方法
}
- 2、AbstractQueuedSynchronizer#acquireSharedInterruptibly()
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
// 调用之前先检测该线程中断标志位,检测该线程在之前是否被中断过
if (Thread.interrupted())
// 若被中断过的话,则抛出中断异常
throw new InterruptedException();
// 尝试获取共享资源锁,小于0则获取失败,此方法由AQS的具体子类实现
if (tryAcquireShared(arg) < 0)
// 将尝试获取锁资源的线程进行入队操作
doAcquireSharedInterruptibly(arg);
}
- 3、Semaphore.FairSync#tryAcquireShared()——公平
protected int tryAcquireShared(int acquires) {
for (;;) { // 自旋的死循环操作方式
if (hasQueuedPredecessors()) // 检查线程是否有阻塞队列
return -1; // 如果有阻塞队列,说明共享资源的许可数量已经用完,返回-1乖乖进行入队操作
int available = getState(); // 获取staste
int remaining = available - acquires; // 计算得到剩下的许可数量
if (remaining < 0 || // 若剩下的许可数量小于0,说明已经共享资源了,返回负数然后乖乖进入入队操作
compareAndSetState(available, remaining)) // 若共享资源大于或等于0,CAS操作占据最后许可证
//不管得到remaining后进入了何种逻辑,操作了之后再将remaining返回
//2、方法会根据remaining的值进行判断是否需要入队操作
return remaining;
}
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
- 4、Semaphore.NonfairSync#tryAcquireShared()——非公平
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
- 5、Semaphore.Sync#nonfairTryAcquireShared()
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
与公平区别就是:非公平不会管队列中是否有在排队的线程,直接符合条件直接CAS
回到第2步,如果获取共享资源锁失败,则执行doAcquireSharedInterruptibly(arg);方法
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
// 调用之前先检测该线程中断标志位,检测该线程在之前是否被中断过
if (Thread.interrupted())
// 若被中断过的话,则抛出中断异常
throw new InterruptedException();
// 尝试获取共享资源锁,小于0则获取失败,此方法由AQS的具体子类实现
if (tryAcquireShared(arg) < 0)
// 将尝试获取锁资源的线程进行入队操作
doAcquireSharedInterruptibly(arg);
}
- 6、AbstractQueuedSynchronizer#doAcquireSharedInterruptibly()
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
// 按照给定的mode模式创建新的结点,模式有两种:Node.EXCLUSIVE独占模式、Node.SHARED共享模式;
final Node node = addWaiter(Node.SHARED); // 6.1 加入队列,返回值是以当前线程创建的Node节点
boolean failed = true;
try {
for (;;) { // 自旋
final Node p = node.predecessor(); // 获取当前结点的前驱结点
if (p == head) { // 若前驱结点为head的话,则当前节点时队列中的第一个节点,最先尝试获取锁
int r = tryAcquireShared(arg);
if (r >= 0) { // 若r >= 0,说明已经成功的获取到了共享锁资源
setHeadAndPropagate(node, r); // 把当前node结点设置为头结点,并且调用doReleaseShared释放一下无用的结点
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && // 是否需要挂起
parkAndCheckInterrupt()) // 阻塞操作,正常情况下,获取不到共享锁,代码就在该方法停止了,直到被唤醒
// 被唤醒后,发现parkAndCheckInterrupt()里面检测了被中断了的话,则补上中断异常,因此抛了个异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
AbstractQueuedSynchronizer#addWaiter()
方法
private Node addWaiter(Node mode) {
//创建当前线程节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//创建pred节点指向尾节点
Node pred = tail;
//尾节点不为null时
if (pred != null) {
//让当前线程节点的前驱节点指向pred节点(尾节点)
node.prev = pred;
//CAS交换pred和node节点
if (compareAndSetTail(pred, node)) {
//pred的后继节点指向node
pred.next = node;
//返回当前线程节点
return node;
}
}
//注意!到达这里有两种情况,一是尾节点为null(队列为空)
//二是CAS替换尾节点失败(因为这个方法是多个线程一起执行的,CAS可能成功,可能失败)
enq(node);
//返回当前线程节点
return node;
}
AbstractQueuedSynchronizer#enq()
方法
private Node enq(final Node node) {
for (;;) {
//尾节点赋值给t
Node t = tail;
//如果尾节点为null,则为空队列
//从这里可以看出, 队列不是在构造的时候初始化的, 而是延迟到需要用的时候再初始化
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
// 这里仅仅是将尾节点指向dummy节点(head),并没有返回
tail = head;
} else {
// 到这里说明队列已经不是空的了, 这个时候再继续尝试将节点加到队尾
node.prev = t;
//交换尾节点和当前节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上面两个方法在ReentrantLock中已给出分析
- 7、Semaphore#release()
public void release() {
sync.releaseShared(1); // 释放一个许可资源
}
- 8、AbstractQueuedSynchronizer#releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 尝试释放共享锁资源,此方法由AQS的具体子类实现
doReleaseShared(); // 自旋操作,唤醒后继结点
return true;
}
return false;
}
- 9、Semaphore.Sync#tryReleaseShared
protected final boolean tryReleaseShared(int releases) {
for (;;) { // 自旋
int current = getState(); // 获取最新的共享锁资源值
int next = current + releases; // 对许可数量进行加法操作
// int类型值小于0,是因为该int类型的state状态值溢出了,溢出了的话那得说明这个锁有多难释放啊,可能出问题了
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next)) //
return true; // 返回成功标志,告诉上层该线程已经释放了共享锁资源
}
}
- 10、AbstractQueuedSynchronizer#doReleaseShared()
private void doReleaseShared() {
for (;;) { // 自旋
Node h = head; // 每次都是取出队列的头结点
if (h != null && h != tail) { // 若头结点不为空且也不是队尾结点
int ws = h.waitStatus; // 那么则获取头结点的waitStatus状态值
if (ws == Node.SIGNAL) { // 若头结点是SIGNAL状态则意味着头结点的后继结点需要被唤醒了
// 通过CAS尝试设置头结点的状态为空状态,失败的话,则继续循环,因为并发有可能其它地方也在进行释放操作
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)){
continue; // loop to recheck cases
}
unparkSuccessor(h); // 唤醒头结点的后继结点
}
// 如头结点为空状态,则把其改为PROPAGATE状态,失败的则可能是因为并发而被改动过,则再次循环处理
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 若头结点没有发生什么变化,则说明上述设置已经完成
// 若发生了变化,可能是操作过程中头结点有了新增或者啥的,那么则必须进行重试,以保证唤醒动作可以延续传递
if (h == head) // loop if head changed
break;
}
}
总结
Semaphore其实就是实现了AQS共享功能的同步器,对于Semaphore来说,资源就是许可证的数量:
- 剩余许可证数(State值) - 尝试获取的许可数(acquire方法入参) ≥ 0:资源可用
- 剩余许可证数(State值) - 尝试获取的许可数(acquire方法入参) < 0:资源不可用
这里共享的含义是多个线程可以同时获取资源,当计算出的剩余资源不足时,线程就会阻塞。
注意:Semaphore不是锁,只能限制同时访问资源的线程数,至于对数据一致性的控制,Semaphore是不关心的。当前,如果是只有一个许可的Semaphore,可以当作锁使用
参考:J.U.C之synchronizer框架:Semaphore