前言
Github:https://github.com/yihonglei/jdk-source-code-reading(java-concurrent)
一 原理
Semaphore(信号量),内部维护一组许可证,通过 acquire 方法获取许可证,如果获取不到,则阻塞;
通过 release 释放许可,即添加许可证。许可证其实是 Semaphore 中维护的一个 volatile 整型 state 变量,
初始化的时候定义一个数量,获取时减少,释放时增加,一直都是在操作 state。
Semaphore 内部基于 AQS (同步器) 实现了公平或分公平两种方式获取资源。
Semaphore 主要用于限制线程数量、一些公共资源的访问。
下面通过实例体验 Semaphore 的含义,然后在从源码角度分析(jdk1.8)Semaphore 的实现原理。
二 实例
1、实例场景
公司每层楼都有一个卫生间,每个卫生间有5个大号坑!
卫生间是公共资源,这里用 Semaphore 来模拟现实的排队上厕所这件事情。
1)通过 acquire 获取锁
卫生间有 5 个坑,通过 acquire 来获取坑,获取到就用,如果没有获取到就阻塞,就憋着,排队等待,
总不能踹开门把人家拽出来吧,我们都是文明人。
2)通过 release 释放锁
上完厕所的人通过 release 释放坑,资源就让出来了,然后排队等待的人通过 acquire 尝试去获取资源上厕所,
获取不到的还是耐心等待。
3)Semphore 公平或非公平获取资源
这个例子举到这个地方,顺便把 Semphore 的公平或不公平获取资源分析下。
Semaphore 两个构造器:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
从代码可以清晰的看到,默认是非公平,fair 传 true 就是公平。
公平:就是一个人来了看到前面有人排队,就老老实实的跟着排。
非公平:就是一个人来了不看是否有排队,非得直接奔着坑去,拉一下门,发现都有人,然后老老实实的去队尾跟着排队。
区别:公平就是看到有人排队直接老实参与排队,非公平就是不看是否有排队,先去试一下,没资源再老老实实的排队。
2、实例代码
定义一个厕所类,里面通过Semaphore设置了5个坑:
package com.jpeony.concurrent.semaphore;
import java.util.concurrent.Semaphore;
/**
* 卫生间有5个坑
*
* @author yihonglei
*/
public class Toilet {
/**
* 5个固定的茅坑
*/
private static Semaphore semaphore = new Semaphore(5, true);
/**
* 茅坑
*/
static class Pit {
private String desc;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
/**
* 获取一个坑
*/
public Pit getPit() throws InterruptedException {
semaphore.acquire();
Pit pit = new Pit();
pit.setDesc("<<<获得坑了>>>");
return pit;
}
/**
* 释放一个坑
*/
public Pit releasePit() {
semaphore.release();
Pit pit = new Pit();
pit.setDesc("@@@释放了坑@@@");
return pit;
}
}
定义一个上厕所的线程,表示谁上厕所:
package com.jpeony.concurrent.semaphore;
/**
* 大便!!!(画面感很强!)
*
* @author yihonglei
*/
public class ShiftThread extends Thread {
private Toilet toilet;
private Integer num;
public ShiftThread(Toilet toilet, Integer num) {
this.toilet = toilet;
this.num = num;
}
@Override
public void run() {
try {
// 获得坑
Toilet.Pit pitAcquire = toilet.getPit();
System.out.println("序号:" + num + ", " +pitAcquire.getDesc());
// 解决大号
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放坑
Toilet.Pit pitRelealse = toilet.releasePit();
System.out.println("序号:" + num + ", " + pitRelealse.getDesc());
}
}
}
测试代码:
package com.jpeony.concurrent.semaphore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Semaphore(信号量)测试
*
* @author yihonglei
*/
public class SemaphoreTest {
public static void main(String[] args) {
try {
Toilet toilet = new Toilet();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 1; i <= 30; i++) {
executorService.execute(new ShiftThread(toilet, i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序分析:
一开始 5 个坑都是空的,会有 5 个人先后获取了坑,其它的耐心等待,当某一个空出来的时候,
再按排队的顺序接着上,然后就是重复获取资源和释放资源的过程,但是最多只能同时有 5 个人在坑里。
这就是控制对公共资源的访问,因为很多资源是有限的,有限的资源就不能过度使用,否则就乱了,
你不能一个坑里蹲两人吧,如果有两个人,哪绝对不是在上厕所,而是在干别的,干啥就不知道了啊...
三 源码分析
1、构造器
1)Semaphore 构造器,permits 为传入的许可证数,默认非公平构造器;
2)Semaphore 构造器,permits 为传入的许可证数,fair 是 boolean 型的,如果传入 true,则公平,否则不公平;
默认使用的是非公平构造器。
NonfairSync 和 FairSync源码:
两者都继承了 Sync 同步器,初始化时都调用了父类构造器,同时都有一个获取信号的方法,稍后再分析获取信号的区别。
Sync 源码:
1)Sync 为 Semaphore 的内部静态类,同时继承了 AQS 同步器,主要是再获取或释放信号的时候通过
同步器的 CAS 算法实现原子更新。
2)构造器调用了 setState 方法,state 为 Semaphore 的一个成员变量,对应 setState 方法源码如下:
/**
* The synchronization state.
*/
private volatile int state;
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
所以从 Semaphore 构造器传进来的 permits 许可证数量,最后赋值到 volatile 变量 state。
volatile 是共享变量,内存可见,可用于线程间通讯,每个线程看到的 state,一定会拿到最新的 state 值。
Semaphore 获取信号或释放信号都是对 state 进行原子性减少或增加的操作。
2、acquire(获取信号量)
获取信号量默认方法源码:
acquire 有其它的重构方法,咱们这里分析默认获取信号量方法,其它的雷同。
获取信号量时,调用的是 Sync 的 acquireSharedInterruptibly 方法,默认参数为 1,
Sync 继承了 AQS,调用的其实是 AQS 的方法源码:
1)判断当前来获取信号量的线程是否中断,如果中断,直接跑线程中断异常。
2)tryAcauireShared 是真正去获取信号量的方法,获取到就返回当前信号量剩余数,也就是还有多少资源,否则就返回-1。
3)如果获取不到信号量,tryAcauireShared 方法返回-1,就会进入 doAcquireSharedInterruptibly 方法,
该方法会将哪些获取不到信号量的线程加入队列里面等待排队。
咱们继续看 tryAcauireShared 是如何处理信号量获取的,tryAcauireShared 方法签名:
该方法在 Semaphore 的静态内部类中有两个实现类:
先看公平的 FairSync:
1)先判断等待队列里面是否有正在等待获取信号的线程,如果有,直接就返回-1,外层代码会把该线程加入等待队列里面,
等待着获取信号量。就好比上厕所,你看到有人排队了,就不要去尝试获取资源了,老老实实排队就行了,厕所肯定是满位,
要不然别人也没傻到有位置不用,闲得没事在哪里排队玩。这就是公平获取信号的逻辑,看到有排队的,老老实实加入排队大军。
2)如果有资源,首先获取可用的资源,然后减掉我们想要获取的资源,得到剩余的资源,也就是 remaining。
判断条件 remaining<0 是防止虽然没有排队的,但是资源刚好占满了,这个时候来获取,必然没有资源,可用为 0,
remaining 就是负数,直接返回负数,外层会把该线程加入等待队列。
如果 remaining 是大于0的,则会执行后面的 compareAndSetState(available,remaining) 通过原子更新信号量方式
来获取信号了,如果更新成功,获取成功,返回 true,这个时候返回的 remaining 就是大于0的,并且这个玩意就是
剩余信号量。所以,当能获取信号量时,返回的int值就是当前剩余信号量。
然后再看非公平 NonfairSync 源码:
有没有发现,非公平相对于公平的代码只是去掉了关于等待队列的判断部分,非公平上来绝不判断
队列里面是否有等待获取信号的线程,而是直接获取资源,获取不到外层处才老老实实的加入等待队列。
就好比上厕所,大家都在排队呢,一个哥们来了,看到排队,不听人说没坑了,也不排队,就是奔着资源去,
然后挨个门拉一遍,发现都有人,才又老老实实的去排队。
小结下公平与非公平区别:
1)公平就是看到有线程等待获取信号了,就跟着排队,不去试着获取信号;
2)非公平就是无视排队,直接尝试获取信号量,获取不到再加入排队大军;
3、release(释放信号量)
释放信号量源码:
1)释放信号量。具体实现源码:
1)获取当前信号数量,也就是state变量的值。
2)当前信号量加上要释放的信号量等于释放后的信号量。
3)next<current 时抛异常,也就是释放后的信号量还不如释放前多,要不是传了负数值或者出现并发导致 state 为负数了。
4)通过 CAS 算法原子操作信号量 state,进行信号量释放,恢复信号量个数,也就是使 state 值增加 releases 个数。
2)如果 tryReleaseShared 尝试释放 state 成功,通过 doReleaseShared 进行后继节点唤醒具体处理:
茅坑让出来了,等着的人就可以用了!等着的人也得保证先来后到,程序处理就麻烦些!
1)获取队列的头节点元素,如果不为null,并且不为尾节点,说白了,就是不止一个人等待,进入判断。
2)如果线程节点是需要唤醒的线程,则进行唤醒,获取资源使用。
3)失败后重试。
4)如果没有后继需要唤醒的节点,则退出,就相当于每人排队上厕所了,让出来资源就空着。
四 Semaphore 总结
1、Semaphore 内部维护一组信号量,即一个 volatile 的整型 state 变量。
2、Semaphore 分为公平或非公平两种方式,获取信号量或释放信号量的本质是对 state 进行原子的减少或增加操作。
3、获取不到信号的线程放在等待队列里面,释放信号的时候会唤醒后继节点。
4、Semaphore 主要用于对线程数量、公共资源(比如数据库连接池)等进行数量控制。
————————————————
版权声明:本文为CSDN博主「街灯下的小草」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yhl_jxy/article/details/87279383