前言:一位朋友问到了我Semaphore类相关的知识,简单看了一下源码复习了一下,写下本篇文章做一个回顾。
希望能够加深自己的印象以及帮助到其他的小伙伴儿们😉😉。
如果文章有什么需要改进的地方还请大佬不吝赐教👏👏。
小威在此先感谢各位大佬啦~~🤞🤞
🏠个人主页:小威要向诸佬学习呀
🧑个人简介:大家好,我是小威,一个想要与大家共同进步的男人😉😉
目前状况🎉:24届毕业生,曾经在某央企公司实习,目前在某税务公司实习👏👏💕欢迎大家:这里是CSDN,我总结知识的地方,欢迎来到我的博客,我亲爱的大佬😘
以下正文开始
Semaphore简单介绍
Semaphore,在中文中有信号量的意思,它被用来限制能同时访问共享资源的线程上限。
可以将Semaphore比喻为停车场,permits(许可)好比停车位的数量,线程好比汽车,这个过程就好像我们限制了停车场汽车停放的数量。当每个汽车获取一个停车位,停车场可共享的停车位数量将减一,反之加一。
通过一个小案例看下Semaphore的用法,然后再进行源码分析。
Semaphore案例详解
首先,我们新建一个semaphore对象,并传入信号量的值为3,代表能同时访问共享资源的线程上限为3;接着循环创建10个线程,让每个线程获取信号量资源,等待线程运行1秒后,释放信号量资源,代码如下:
public class TestSemaphore {
public static void main(String[] args) {
//创建semaphore对象
Semaphore semaphore = new Semaphore(3);
for (int i=0;i<10;i++){
new Thread(()->{
try{
//获取信号量资源
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.err.println(Thread.currentThread().getName()+"正在运行中……");
sleep(1000);
System.out.println(Thread.currentThread().getName()+"运行已结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//释放信号量资源
semaphore.release();
}
}).start();
}
}
}
运行测试案例可以得到下面的结果:
由此可见,semaphore限制了同时访问共享资源的线程数,每次只能三个线程获取共享资源。
源码剧透
在前面的文章中详细记录过AQS(Abstract Queued Synchronizer)的知识,不得不说AQS是真的强大,在Semaphore类的源码实现中也用到了AQS的原理,这点知识忘记的大佬可以看下前面AQS的文章。
获取许可(acquire)
讲到线程获取许可,还需要从Semaphore的构造方法详细说起,Semaphore类位于JUC(java.util.concurrent)包下:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
构造方法中新建了一个非公平的同步器方法,并将许可值传入了进去,点入NonfairSync( )一看,大吃一惊!
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
}
它居然调用的是父类的方法,耐住性子,点入super发现,我的老天爷,简直不敢看:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
从这块源代码中可以看出,同步器类继承了AQS,Semaphore’类的底层实现还是依靠了AQS先进先出的阻塞队列。而且传入的permits许可值被赋予了AQS的state状态字段。
当线程获取资源时调用的acquire()方法,我们搜索acquire看看源码吧:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
上述代码表示线程可中断地获取1个许可值,点入acquireSharedInterruptibly(1)方法来看看它的实现:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
首先对当前线程进行一个判断,如果当前线程被中断,那么直接抛出异常,如果没有,则会调用tryAcquireShared(arg)方法,此方法是由子类具体实现的。在之前的AQS文章中,尝试获取共享资源时记录过这块的知识。如果tryAcquireShared(arg) 方法返回的是大于0的数,表示获取成功,如果线程获取许可失败,该方法返回值表示剩余的资源数,tryAcquireShared(arg)方法如下:
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
这个方法有四个具体的实现(四大天王),我们点入想看的Semaphore实现类中去:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
上述代码又调用到了nonfairTryAcquireShared(),点入 nonfairTryAcquireShared(非公平尝试获取共享资源)方法查看得到:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
这段代码意思是,拿到state的状态值,减去传入的许可数,remaining就是剩余的许可数,如果remaining不小于0,则调用CAS机制,安全的更改state的值,将remaining返回。
如果当前state状态为0,减去acquire之后变成负数,接下来判断remaining<0为TRUE,不会在CAS更新state的值,短路,直接返回remaining(负数)。
如果返回的remaining小于0,则会调用doAcquireSharedInterruptibly(arg)方法,这里又涉及到了AQS中的常用方法,这里可以看注释理解:
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//将节点加入阻塞队列尾部
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) {
//如果获取到许可,唤醒node节点,释放p(头结点)
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//如果前面没有获取成功,将线程阻塞住并将state设置为-1
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private Node addWaiter(Node mode) {
/* 这个可以参考上面Node的构造方法
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
*/
//构造新的等待线程节点
Node node = new Node(Thread.currentThread(), mode);
//新建临时节点pred指向尾节点
Node pred = tail;
//队列不为空的话,通过CAS机制将node放到队列尾部
if (pred != null) {
//将node的prev域指向尾节点
node.prev = pred;
//通过CAS机制将node放到队列尾部
if (compareAndSetTail(pred, node)) {
//将原来尾节点的next域指向当前node节点,node现在为尾节点
pred.next = node;//形成双向链表
return node;
}
}
//如果队列为空的话
enq(node);
return node;
}
在多线程并发情况下,如果有多个线程同时争夺尾节点的位置,会调用enq(node)方法,使用CAS自旋机制挂到双向链表的尾部,下面是源码:
private Node enq(final Node node) {
//死循环(自旋)
for (;;) {
Node t = tail;
//尾节点为null,说明头结点也为null,可能是还没有创建队列的时候
if (t == null) {
//多线程并发情况下,利用CAS机制创建头结点和尾节点,CAS保证此时只有一个头节点被创建,下次自旋时,就会满足队列不为空的条件
if (compareAndSetHead(new Node()))
tail = head;
} else {
//如果存在尾节点,将当前节点的prev域指向尾节点
node.prev = t;
//利用CAS机制完成双向链表的绑定,让之前尾节点指向当前node节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
接下来看一看compareAndSetTail方法使用CAS乐观锁机制的方法源码:
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
如果当前的前驱节点表示头结点,并且获取资源成功,那么直接将当前线程设为头结点,释放之前头结点与后继节点的链接,帮助垃圾回收(GC),如果前面当前节点的前驱不为头结点或者没有获取到资源,那么会调用shouldParkAfterFailedAcquire(Node pred, Node node)方法来判断当前线程是否能够进入waiting状态,如果可以进入,并且进入到了阻塞状态,那会阻塞,直到调用了LockSupport中的unpark()方法唤醒线程。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//保存前驱节点的状态
/*
提示:
当waitState>0时,表示该线程处于取消状态(线程中断或者等待锁超时),需要移除线程;
当waitState=0时,默认值,表示初始化状态,表示线程还未完成初始化操作;
当waitState<0,表示有效状态,线程处于可唤醒状态。
*/
int ws = pred.waitStatus;
//等待唤醒后置节点,SIGNAL为-1
if (ws == Node.SIGNAL)
return true;
//如果前置节点不是正常的等待状态(CANCELLED结束状态),那么从当前节点开始往前寻找正常的等待状态
if (ws > 0) {
do {
//后面的节点断开与前驱节点的链接
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//双向连接
pred.next = node;
} else { //小于0时,可能为共享锁
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果前驱节点的SIGNAL值为-1,会返回true。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法内部也使用了CAS锁机制,源码:
private static final boolean compareAndSetWaitStatus(Node node,
int expect,
int update) {
return unsafe.compareAndSwapInt(node, waitStatusOffset,
expect, update);
}
如果shouldParkAfterFailedAcquire(Node pred, Node node)方法返回true,则会调用parkAndCheckInterrupt()方法阻塞当前线程,线程等待,如果线程被中断过则返回true:
private final boolean parkAndCheckInterrupt() {
// 调用park让线程进入wait状态
LockSupport.park(this);
// 检查线程是否中断过。
return Thread.interrupted();
}
如果线程在等待的过程中被中断过,那么获取到资源后会通知线程中断:
/**
* Convenience method to interrupt current thread.
*/
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
释放许可(release)
在Semaphore类中找到release()方法,
public void release() {
sync.releaseShared(1);
}
调用releaseShared(1)方法释放1个许可,点入releaseShared(1)方法的:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
继续点入tryReleaseShared(arg)查看源码得:
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
这个类有三个实现类,点击Semaphore的实现类中查看方法:
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;
}
}
这个方法和上面获取许可时的方法类似,也是获取当前的状态值,将当前状态值和释放的许可值相加,最后调用CAS方法替换返回即可。
替换成功后,返回true,将继续向下进行doReleaseShared()方法,这个也在AQS中详细提到过,这里由于篇幅原因就不再复述了。
小结
其实从此可以看出,Semaphore类大多数用到了AQS源码的相关知识,因此AQS的知识还是挺重要的,这块也是面试中的重点,希望我们都能把握住哦~~~
好了,本篇文章就先分享到这里了,后续会继续分享其他方面的知识,感谢大佬认真读完支持咯~
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起讨论😁
希望能和诸佬们一起努力,今后我们顶峰相见🍻
再次感谢各位小伙伴儿们的支持🤞