多线程之Semaphore
Semaphore
1、Semaphore概念
Semaphore,俗称信号量,它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的。
Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量。
1.1、PV操作
PV操作是操作系统一种实现进程互斥与同步的有效方法。PV操作与信号量(S)的处理相关,P表示通过的意思,V表示释放的意思。用PV操作来管理共享资源时,首先要确保PV操作自身执行的正确性。
P操作的主要动作是:
①S减1;
②若S减1后仍大于或等于0,则进程继续执行;
③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度。
V操作的主要动作是:
①S加1;
②若相加后结果大于0,则进程继续执行;
③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度。
2、Semaphore 常用方法
2.1、构造器确定资源数目
permits 表示许可证的数量(资源数)
fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程 。
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
查看源码进去之后,可以看到最终到了:
protected final void setState(int newState) {
state = newState;
}
可以看到传入进去的参数将会作为资源数目的多少来进行定义。
3、常用方法
public void acquire() throws InterruptedException
public boolean tryAcquire()
public void release()
public int availablePermits()
public final int getQueueLength()
public final boolean hasQueuedThreads()
protected void reducePermits(int reduction)
protected Collection<Thread> getQueuedThreads()
方法名称 | 语义 | |
---|---|---|
acquire() | 表示阻塞并获取许可 | |
tryAcquire() | 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞 | |
release() | 表示释放许可 | |
int availablePermits() | 返回此信号量中当前可用的许可证数。 | |
int getQueueLength() | 返回正在等待获取许可证的线程数。 | |
boolean hasQueuedThreads(): | 是否有线程正在等待获取许可证。 | |
void reducePermit(int reduction) | 减少 reduction 个许可证 | |
Collection getQueuedThreads() | 返回所有等待获取许可证的线程集合 |
4、应用场景
可以用于做流量控制,特别是公用资源有限的应用场景
示例:
/**
* @Description 有三个许可,五个线程来抢。效果:三个线程执行成功,另外两个线程进行排队
* @Author liguang
* @Date 2022/03/18/11:13
*/
public class SemphoreTestOne {
public static void main(String[] args) {
Semaphore windows = new Semaphore(3);
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
windows.acquire();
System.out.println("开始卖票");
Thread.sleep(5000);
System.out.println("购票成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
windows.release();
}
}).start();
}
}
}
控制台展示:
开始卖票
开始卖票
开始卖票
-----------------停顿5s钟----------------
购票成功
购票成功
开始卖票
开始卖票
购票成功
效果上:许可只有三个,但是有多个线程,那么多个线程来获取得到三个有限的资源的时候,对于已经获取得到了的线程来说,是没有任何影响,而没有获取得到的线程只能够排队等待资源。
5、源码分析
这里将会涉及到共享锁的一些概念。
从两行代码中来进行分析:
Semaphore windows = new Semaphore(3);
try {
windows.acquire();
xxxxx(业务方法);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
windows.release();
}
那么在acquire和release就可以实现这样的逻辑,那么看下具体的实现逻辑。
5.1、acquire
只需要来分析这段代码即可:
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
那么看下尝试获取得到共享锁,当对应的值小于0的时候,才会去将线程进行阻塞。
首先看下尝试获取得到共享锁的方法(非公平场景下来进行分析):
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
// 1、当许可的数量小于0(表示的当前资源不足);2、大于等于0的时候,表示资源还是足够的,可以分配给当前线程
if (remaining < 0 || compareAndSetState(available, remaining))
// 许可剩余数量
return remaining;
}
}
线程进来的时候,进行比较并交换,将许可证数量减少一个,这里有两种情况:
1、然后如果是大于等于0的,比较并交换即可;
2、如果是小于0的,那么将会直接进入到阻塞队列中去;
对于没有获取得到成功的,那么将继续循环操作。最终将返回许可的剩余数量。
对应的流程图如下所示:
那么在回到上面的方法中来,只有当许可小于0的时候,才会将对应的线程进行阻塞。而大于等于0的,则是直接获取得到资源进行操作了
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 这里和之前的还是有区别的,这里使用到节点的另外一种状态。SHARED,共享状态。构建一个共享队列来进行创建
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 如果是头节点!那么这里再来尝试获取得到一次锁,如果得到的
int r = tryAcquireShared(arg);
// 如果获取得到的资源大于等于0,那么将会进行下面的操作
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);
}
}
那么下面只需要关注一下setHeadAndPropagate方法即可
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 在这里要来唤醒下一个节点。这里直接在这里来进行唤醒了!
if (s == null || s.isShared())
// 可以看到释放锁也是在这里来进行操作的。在这里直接来通知后面的排队的线程来进行操作。
// 唤醒后面的线程来抢锁
doReleaseShared();
}
}
5.2、release
释放也是非常简单的,直接上源码:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
那么继续看下后面的方法:
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;
}
}
直接获取得到原来的值,然后进行相加重新设置+1即可。
看看释放锁的过程:
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
unparkSuccessor(h);
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
可以看到是将节点状态SINAL修改成0,然后唤醒后面的线程。这里就跟之前的是类似的。
6、总结
这个源码比较简单,所以不过来过多解释。无非是检测到了状态是SHARED并且不为null的,直接进行唤醒来尝试通知下一个线程即可。
但是使用的时候需要注意的一点是:要有效率的来获取得到许可数量。
6.1、DEMO1
线程获取得到许可证的数量不能够大于许可证原始数量
如下所示:
/**
* @Description 获取得到许可过大。注意这里的许可数量设置
* @Author liguang
* @Date 2022/03/18/11:13
*/
public class SemphoreTestTwo {
public static void main(String[] args) {
Semaphore windows = new Semaphore(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
windows.acquire(6);
System.out.println("开始卖票");
Thread.sleep(5000);
System.out.println("购票成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
windows.release();
}
}).start();
}
}
}
这种操作最终将会导致所有的线程都将会在阻塞队列中进行排队,等待被激活。无法被唤醒,除非是因为操作不当,因为中断引起。
6.2、DEMO2
线程获取得到多少许可证,在释放的时候就要释放多少许可证
再来一个例子:
/**
* @Description 获取得到许可证的的数量要和释放许可证的数量保持一致
* @Author liguang
* @Date 2022/03/18/11:13
*/
public class SemphoreTestTwo {
public static void main(String[] args) {
Semaphore windows = new Semaphore(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
windows.acquire(3);
System.out.println("开始卖票");
Thread.sleep(5000);
System.out.println("购票成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
windows.release(3);
}
}).start();
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?