Semaphore
流程介绍
当线程调用了 acquire , state 值代表的资源数足够使用,那么请求线程将会获得同步状态即对共享资源的访问权,并更新 state 的值(一般是对state值减1),但如果state值代表的许可数已为0,则请求线程将无法获取同步状态,线程将被加入到同步队列并阻塞,直到其他线程释放同步状态(一般是对state值加1)才可能获取对共享资源的访问权
信号量主要用于两个目的:
一个是用于多个共享资源的互斥使用;
一个是用于并发线程数的控制。
源码
1. Semaphore源码
public class Semaphore implements java.io.Serializable {
private final Sync sync;
//-----------------构造器
public Semaphore(int permits) {//创建了非公平锁
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {//根据true false 决定创建公平锁还是非公平锁
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
//-----------------锁
abstract static class Sync extends AbstractQueuedSynchronizer {}
//-----------------非公平锁 继承了锁
static final class NonfairSync extends Sync {}
//-----------------公平锁 继承了锁
static final class FairSync extends Sync {}
//-----------------加锁方法 该方法可以被中断
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);//AQS的方法,进入该方法后,先判断线程是否被中断了
}
//-----------------释放锁方法
public void release() {
sync.releaseShared(1);
}
//·············其他
}
2. Sync源码
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
Sync(int permits) {//构造方法
setState(permits);//AQS的方法
}
final int getPermits() {
return getState();//AQS的方法
}
//非公平锁加锁方法(被final修饰)
//查看是否有可获取的锁,如果有则获取成功,没有则进队列等待
final int nonfairTryAcquireShared(int acquires) {
// 自旋直到无许可或者状态位赋值成功
for (;;) {
//获取全部可获取的许可证数量
int available = getState();
//减去要获取的许可证数量
int remaining = available - acquires;
// 如果剩余数量小于0则直接返回(返回后会将该线程进入AQS中的doAcquireSharedInterruptibly方法,线程会被挂起,直到被唤醒),
if (remaining < 0 ||
//CAS替换锁状态值
compareAndSetState(available, remaining))
return remaining;
}
}
//释放锁方法(公平锁和非公平所共用) 自旋直到释放锁成功
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");
//如果许可证释放成功,当前线程进入到AQS的doReleaseShared方法,
//唤醒队列中等待许可的线程。
if (compareAndSetState(current, next))//CAS更改锁状态
return true;
}
}
final void reducePermits(int reductions) {
//--------------
}
final int drainPermits() {
//-----------------
}
}
3. NonfairSync源码
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {//AQS的方法
return nonfairTryAcquireShared(acquires);//Sync的方法
}
}
当一个线程A调用acquire方法时,会直接尝试获取许可证,而不管同一时刻阻塞队列中是否有线程也在等待许可证,如果恰好有线程C调用release方法释放许可证,并唤醒阻塞队列中第一个等待的线程B,此时线程A和线程B是共同竞争可用许可证,不公平性就体现在:线程A没任何等待就和线程B一起竞争许可证了。
4. FairSync源码
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {//AQS的方法
for (;;) {
//检查阻塞队列中是否有等待的线程
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
//remaining小于0就表示获取锁失败了
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
和非公平策略相比,FairSync中多一个对阻塞队列是否有等待的线程的检查,如果没有,就可以参与许可证的竞争;如果有,线程直接被插入到阻塞队列尾节点并挂起,等待被唤醒。
4.1 compareAndSetState
如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
5. AQS源码
//加锁的方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//如果当前线程是被中断的,则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取锁,返回剩余共享锁的数量 如果大于等于0,则表示锁获取成功,如果小于0,则获取失败
if (tryAcquireShared(arg) < 0)//该方法被公平锁和非公平锁重写
//可获取的锁的数量 小于0则将当前线程加入等待队列挂起,自旋
doAcquireSharedInterruptibly(arg);//AQS自己的方法
}
//加锁方法
//返回负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int arg) {//被子类公平锁和非公平锁重写
throw new UnsupportedOperationException();
}
//释放锁
public final boolean releaseShared(int arg) {
//自旋直到释放锁成功
if (tryReleaseShared(arg)) {//该方法被Sync重写
//如果释放成功,则调用下面的方法来唤醒AQS队列中等待的线程
doReleaseShared();//AQS自己的方法
return true;
}
return false;
}
//释放锁
//arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False
protected boolean tryReleaseShared(int arg) {//被Sync锁重写了,公平锁和非公平锁共用
throw new UnsupportedOperationException();
}
5.1 doAcquireSharedInterruptibly
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
//加入等待队列的尾部 将当前线程封装为一个Node
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {//死循环去获取共享锁(可以同时被多个线程获取)
//获取当前节点的上一个节点
final Node p = node.predecessor();
//如果上一个节点是头节点,当前线程就需要自旋获取锁,因为头节点是一个虚节点,则表示当前节点是排在最前面的等待获取锁的线程节点
if (p == head) {
//重试获取锁
//从剩余可获取的锁中获取锁 如果r大于等于0,说明获取成功了
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);
}
}
5.2 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
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 通过CAS方式设置队列的尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
5.3 doReleaseShared
private void doReleaseShared() {
for (;;) {
Node h = head;
//头节点不是null 也不等于尾节点说明此时队列中还有节点存在,则表示有线程被阻塞挂起等待
if (h != null && h != tail) {
int ws = h.waitStatus;//获取头节点的状态
if (ws == Node.SIGNAL) {//如果头节点是SIGNAL状态,则意味着头结点正在运行,后继结点所对应的线程需要被唤醒。
// 修改头结点,现在后继节点要成为头结点了,状态设置初始值
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)){
continue; // loop to recheck cases
}
//唤醒后继节点
unparkSuccessor(h);
// 队列中没有等待线程,只有一个正在运行的线程。
// 将头结点设置为 PROPAGATE 标志进行传递唤醒
//如果状态为0 并且修改状态失败,则继续下一次循环
}else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)){
continue; // loop on failed CAS
}
}
// 如果头结点发生变化有一种可能就是在 acquireShared 的时候会调用setHeadAndPropagate 导致头结点变化,则继续循环。
// 从新的头结点开始唤醒后继节点。
if (h == head) // loop if head changed
break;
}
}
5.4 shouldParkAfterFailedAcquire
// 检查并更新无法获取的节点的状态。 如果线程应阻塞,则返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//SIGNAL这个状态就有点意思了,它不是表征当前节点的状态,而是当前节点的下一个节点
//的状态。当一个节点的waitStatus被置为SIGNAL,就说明它的下一个节点(即它的后继
// 节点)已经被挂起了(或者马上就要被挂起了),因此在当前节点释放了锁或者放弃获取
// 锁时,如果它的waitStatus属性为SIGNAL,它还要完成一个额外的操作——唤醒它的后继节点。
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
// 前驱节点的 ws > 0, 则为 Node.CANCELLED 说明前驱节点已经取消了等待锁(由于超时或者中断等原因)
// 既然前驱节点不等了, 那就继续往前找, 直到找到一个还在等待锁的节点
// 然后我们跨过这些不等待锁的节点, 直接排在等待锁的节点的后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前驱节点的状态既不是SIGNAL,也不是CANCELLED
// 用CAS设置前驱节点的ws为 Node.SIGNAL,给自己定一个闹钟
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
5.5 enq
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { //如果尾部节点为空
if (compareAndSetHead(new Node()))//则初始化一个节点,并将其设置为头节点
tail = head;//让尾部节点也指向头节点 (此时头节点和尾节点均指向了同一个空节点)
} else {//如果尾部节点不为空
node.prev = t;//将参数节点的上一个节点指向尾节点
if (compareAndSetTail(t, node)) {//将参数节点设置为尾部节点
t.next = node;//将尾部节点的下一个节点指向参数节点
return t;//将尾部节点返回
}
}
}
}
5.6 setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
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.7 setHead
//将传入的node设置为头节点 因为在AQS中头节点其实是一个虚拟的节点,里面的thread等是null,因此这里设置了之后将thread 和prev设置为null
//这里其实就相当于变相的将该节点出队了。
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
5.8 cancelAcquire
private void cancelAcquire(Node node) {
// 忽略调空节点
if (node == null)
return;
//将当前节点的线程信息设为null,即将该节点设置为虚节点
node.thread = null;
// 通过前驱节点,跳过取消状态的node
Node pred = node.prev;
while (pred.waitStatus > 0){
node.prev = pred = pred.prev;
}
// 获取过滤后的前驱节点的后继节点
Node predNext = pred.next;
// 把当前node的状态设置为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点
// 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 如果当前节点不是尾节点,
// 1.判断当前节点前驱节点的是否为SIGNAL
// 2.如果不是,则把前驱节点设置为SINGAL看是否成功
// 如果1和2中有一个为true,再判断前驱节点的线程是否为null
// 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
非公平锁获取锁成功的流程图
非公平锁获取锁失败的流程图
下面完整的跑一遍流程:
- 使用构造方法创建一个 Semaphore 对象,默认底层 new 了一个非公平锁,传入的资源数被父类(AQS)的构造方法使用,初始化了
state
变量。 - 调用 acquire 方法,这个方法直接调用了
acquireSharedInterruptibly()
他首先调用了 Sync 子类中的 tryAcquireShared(因为公平锁和非公平锁的缘故) 如果获取资源失败需要调用doAcquireSharedInterruptibly()
这个方法和独占锁中的acquireQueued
方法如出一辙,首先加入等待队列,然后如果等待队列中只有这一个等待的线程则自旋获取锁。如果获取到了锁并且还有可用就尝试唤醒他之后等待的线程,也就是调用了setHeadAndPropagate()
,否则的话直接等待。 - 这里很巧妙的一点,当我们自旋获取到了锁就说明在自旋的这段时间中有线程释放了资源,然后我们发现资源数还是大于 0 ,那么可以让更多的线程运行(这个时候肯定是新的线程加入了同步队列,也就解释了为什么要使用死循环来执行这段代码),所以才调用了
setHeadAndPropagate()
这个方法里面不仅仅设置了头结点,还调用了doReleaseShared()
这看起来是需要在 release 方法中调用的在 acquire 中也调用了。 - 好了,现在如果调用
release
方法 ,他首先调用releaseShared
之后,里面的逻辑是:先释放锁调用tryReleaseShared()
,如果成功需要唤醒同步队列中等待的线程。所以会调用doReleaseShared
。这里面的代码我们已经说过好几次了,因为在很多地方都调用了他。
参考链接
- https://blog.csdn.net/weixin_30408675/article/details/97935871?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2defaultCTRLISTdefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2defaultCTRLISTdefault-1.no_search_link
- https://blog.csdn.net/b_x_p/article/details/105182145?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2defaultBlogCommendFromBaidudefault-2.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2defaultBlogCommendFromBaidudefault-2.no_search_link
- https://blog.csdn.net/qq_36882793/article/details/103124935?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2defaultBlogCommendFromBaidudefault-4.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2defaultBlogCommendFromBaidudefault-4.no_search_link
- https://blog.csdn.net/wanglun1010/article/details/91980846
相关知识点
1.FIFO(first-in first-out) 先进先出队列
本质是一个数组,从队尾添加元素,从队首移除元素。
缓存一开始没有数据,并且用一个变量 rear 指示下一个存入缓存的索引地址,这里下一个存放的位置就是 0,用另一个变量 front 指示下一个存入缓存的索引地址,并且下一个读出数据的索引地址也是 0。目前队列中是没有数据的,也就是不能读出数据,队列为空的判断条件在这里就是两个索引值相同。
开始存放数据,如下图,我们存放了6个数据:
在这里可以看到队列中加入了 6 个数据,并且每加入一个数据后队尾索引加 1,队头不变,这就是数据加入队列的过程。但是缓存空间只有 7 个,如何判断队列已满呢?
如果只是先一次性加数据到队列中,然后再读出数据,那这里的判断条件显然是队尾索引为 6,但实际上是在加入数据的同时也可能出现有数据已出队的情况,比如:
通过以下算法即可:
(rear + 1) % 7 == front
如果true 则表示队列已满
你可以发现这个算法的巧妙。通过%运算将索引又从 6 返回到了 0 处,这是实现循环队列的关键之处。通过该算法就能知道队列是否已满了。
刚才说过通过%运算可以实现索引值的循环,所以当索引为 6 的时候,一般思维是通过 if 判断语句将其定位到下一个索引位置,而这里通过以下算法即可将其重新定位:
rear = (rear + 1) %7
这样当 rear 等于 6 的时候下一个索引就是 0 了。非常巧妙的实现了数值的循环。这个时候就出现了如下情况:
队尾索引跑到了队头索引的前头。并且周而复始,这就是循环队列了,充分的利用了空间。
但你有没有发现其实在有 7 个空间的情况下其实只能存放 6 个数据,另一个数据空间是没法使用的,为什么呢?看看以下两种情况:
这里一种为队列为空的情况,有一种队列已满的情况,这个时候到底是空还是满的单靠这两个变量是无法判断的,这个时候就需要增加一个变量指示队列已满的情况,并且需要加入判断语句,降低了运行效率,所以建议采用留空的方式进行统一处理。
队列空的算法就是队头队尾索引相同
front == rear
这样入队出队操作都有了,也就算完成了基本操作,实际上有时候需要获取整个队列存放的数量,这时又涉及到了一个有意思的公式:
获取整个队列存放的数量:
length = (rear – front + 7) % 7
来看一看这个公式的巧妙性。当出现如下 rear 在后,front 在前这种正常情况时,只要两种相减即可得到队列的长度。
但实际上对于循环队列来说 rear 在前,front 在后也是再正常不过的事情,如下:
这个时候又该如何获取呢?就是利用上面的公式了。通过它就能适应这两种情况。
2.CLH
2.1 概念
AQS内部维护着一个FIFO队列,即CLH队列。AQS的同步机制便是依据CLH实现的。
CLH队列是FIFO的双端双向队列,实现公平锁。线程通过AQS获取锁失败,就会将线程封装成一个Node节点,插入队列尾。当有线程释放锁时,后尝试把队头的next节点占用锁。
2.2 CLH结构
理解:
CLH内部是由node节点组成的,node节点是AQS的内部类。
2.3 Node
node中包含了线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next)。一个node表示一个线程。
static final class Node {
/** 共享 */
static final Node SHARED = new Node();
/** 独占 */
static final Node EXCLUSIVE = null;
/**
* 因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态;
线程获取锁的请求已经取消了
*/
static final int CANCELLED = 1;
/**
* 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
线程已经准备好了,就等资源释放了
waitStatus=-1的作用,主要是告诉释放锁的线程:后面还有排队等待获取锁的线程,请唤醒他
*/
static final int SIGNAL = -1;
/**
* 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,改节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
节点在等待队列中,节点线程等待唤醒
*/
static final int CONDITION = -2;
/**
* 表示下一次共享式同步状态获取将会无条件地传播下去
*/
static final int PROPAGATE = -3;
/** 等待状态 */
volatile int waitStatus;
/** 前驱节点 */
volatile Node prev;
/** 后继节点 */
volatile Node next;
/** 获取同步状态的线程 */
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
在AQS中的队列是一个FIFO队列,它的head节点永远是一个虚拟结点(dummy node), 它不代表任何线程,因此head所指向的Node的thread属性永远是null。但是我们不会在构建过程中创建它们,因为如果没有争用,这将是浪费时间。而是构造节点,并在第一次争用时设置头和尾指针。只有从次头节点往后的所有节点才代表了所有等待锁的线程。也就是说,在当前线程没有抢到锁被包装成Node扔到队列中时,即使队列是空的,它也会排在第二个,我们会在它的前面新建一个虚拟节点。
参考链接
-
FIFO https://blog.csdn.net/weixin_42876465/article/details/88356757
-
CLH https://blog.csdn.net/weixin_40391011/article/details/104701988