AQS
、 ReentrantLock
和 CountDownLatch
的原理。在今天的文章中我们再学习一个 JUC
下的工具类- Semaphore
。
今天的文章中我们会先介绍下Semaphore
的使用,然后通过源码来学习下其是如何实现的。
Semaphore(信号量)
,是JUC
包下的一个工具类,我们可以通过其限制执行的线程数量,达到限流的效果。
当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束。
Semaphore
的使用也是比较简单的,我们创建一个Runnable
的子类,如下:
private static class MyRunnable implements Runnable {
// 成员属性 Semaphore对象
private final Semaphore semaphore;
public MyRunnable(Semaphore semaphore) {
this.semaphore = semaphore;
}
public void run() {
String threadName = Thread.currentThread().getName();
// 获取许可
boolean acquire = semaphore.tryAcquire();
// 未获取到许可 结束
if (!acquire) {
System.out.println("线程【" + threadName + "】未获取到许可,结束");
return;
}
// 获取到许可
try {
System.out.println("线程【" + threadName + "】获取到许可");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
semaphore.release();
System.out.println("线程【" + threadName + "】释放许可");
}
}
}
测试方法如下:
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i <= 10; i ++) {
MyRunnable runnable = new MyRunnable(semaphore);
Thread thread = new Thread(runnable, "Thread-" + i);
thread.start();
}
}
执行结果如下
Semaphore
的类图如下图所示:
Sync
是Semaphore
的一个内部类,该类继承AQS
,这个类又有公平和非公平的两个子类,这个内置的同步器实现Semaphore
的功能。
在Semaphore
中提供了两个构造方法,如下:
// 指定许可数量
public Semaphore(int permits) {
// sync属性赋值 默认未非公平实现
sync = new NonfairSync(permits);
}
// 指定许可数量和是否公平实现
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
内置同步器的构造方法如下:
FairSync(int permits) {
super(permits);
}
NonfairSync(int permits) {
super(permits);
}
Sync(int permits) {
// 用state属性记录许可量
setState(permits);
}
这些构造方法的逻辑是比较简单的哈,相信大家还记得在AQS
中有一个state
属性,当创建Semaphore
时会将传递过来的许可量设置到同步器的state
值,并将创建的同步器对象赋值给Semaphore
中的sync
属性。
在Semaphore
中提供了如下的获取许可的方法
void acquire() throws InterruptedException
获取一个许可,会阻塞等待其他线程释放许可void acquire(int permits) throws InterruptedException
获取指定的许可数 ,会阻塞等待其他线程释放void acquireUninterruptibly()
获取一个许可 会阻塞等待其他线程释放许可 可被中断void acquireUninterruptibly(int permits)
获取指定的许可数 会阻塞等待其他线程释放许可 可被中断boolean tryAcquire()
尝试获取许可 不会进行阻塞等待boolean tryAcquire(int permits)
尝试获取指定的许可数 不会阻塞等待boolean tryAcquire(long timeout, TimeUnit unit)
尝试获取许可 可指定等待时间boolean tryAcquire(int permits, long timeout, TimeUnit unit)
尝试获取指定的许可数 可指定等待时间由于篇幅有限,我们这里不全部介绍了,我们只介绍下acquire()
和tryAcquire()
两个方法,其他的方法实现大家自行查看吧,相差也不是很大。
tryAcquire
方法这个方法的返回值表示是否获取许可成功,不会阻塞等待其他线程释放许可,没有许可了会直返返回false,其源码如下:
public boolean tryAcquire() {
// 调用Semaphore.Sync中的nonfairTryAcquireShared方法
return sync.nonfairTryAcquireShared(1) >= 0;
}
final int nonfairTryAcquireShared(int acquires) {
// 自旋
for (;;) {
// 获取剩余的许可量
int available = getState();
// 扣减需要的信号量后的值
int remaining = available - acquires;
// 信号量不足 或者CAS替换state失败 返回扣减后的信号量值
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
这个方法的逻辑比较简单,判断许可量是否充足,充足的话CAS
修改state
的值。判断分配所需数量后的值是否大于等于0。
acquire
方法这个方法没有返回值,当许可不足时会阻塞线程等待其他线程释放许可,其源码如下:
public void acquire() throws InterruptedException {
// 调用AQS中的方法
sync.acquireSharedInterruptibly(1);
}
// AQS中的方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// tryAcquireShared是个模板方法,需要子类去实现
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
在acquire
的方法中,会调用到AQS
中的acquireSharedInterruptibly
方法,在这个方法中用到了模板方法模式,tryAcquireShared
方法是一个模板方法,需要子类去实现。接下来我们分别看看在Semaphore
中的公平和非公平模式都是如何实现的。
公平模式下的tryAcquireShared
方法实现如下:
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;
}
}
非公平模式下的tryAcquireShared
方法实现如下:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
// 自旋
for (;;) {
// 获取剩余的许可量
int available = getState();
// 扣减需要的信号量后的值
int remaining = available - acquires;
// 信号量不足 或者CAS替换state失败 返回扣减后的信号量值
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
公平和非公平模式的实现的区别是在公平模式的实现中会先判断是否已经存在阻塞的线程了,存在的话不会再去竞争获取许可了。
AQS.doAcquireSharedInterruptibly
方法的逻辑如下:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 添加到AQS的阻塞队列中
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);
}
}
释放许可的方法为release
方法,其源码如下:
public void release() {
sync.releaseShared(1);
}
// AQS中的方法
public final boolean releaseShared(int arg) {
// 模板方法 需要子类实现
if (tryReleaseShared(arg)) {
doReleaseShared();
// 成功
return true;
}
// 失败
return false;
}
Semaphore.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修改state
if (compareAndSetState(current, next))
return true;
}
}
AQS.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, 0))
continue; // loop to recheck cases
// 唤醒下个节点 LockSupport.unpark
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
至此我们今天的文章就结束了,希望大家通过本文能够知道Semaphore
的作用及如何使用,并能理解其实现的原理。
Semaphore
是在AQS
基础之上实现的一个工具,AQS
的实现原理我们在前面的文章里已经介绍了,这里我们就没有详细的再去介绍。
Semaphore
的剩余许可量是通过AQS
中的state
属性进行的记录,获取许可是将该值进行减少,释放许可是将该值进行增加,当没有足够的许可时,线程会加入到阻塞队列中等待其他线程释放许可并唤醒。
感谢您的阅读,如果感觉有帮助,欢迎关注公众号:“Bug搬运小能手”。