Semaphore,信号量通常用于限流。
public static void main(String[] args){
Semaphore s = new Semaphore(3, true);
for(int i = 1; i <= 10; i++){
new Thread(() -> {
try {
s.acquire();
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
s.release();
}
}, "t"+i).start();
}
}
构造函数中指定信号数量permits,最终该值被赋给了AQS中的state变量,state的取值代表当前可以获取的信号量总数。acquire(n)尝试state-n,release(n)尝试state+n。
接下来详细讲解几个主要函数
public Semaphore(int permits, boolean fair) {
//true:公平;false:非公平
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
无论公平还是非公平,最终都会调用AQS的setState,设置state值为permits。这里很简单,自己看一看源码就好了。
protected final void setState(int newState) {
state = newState;
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//首先检查当前线程的中断状态位,如果被中断了,就抛出InterruptedException。
//注意,interrupted()会重置中断状态位,即抛出异常之后,该线程中断状态会被抹去。
if (Thread.interrupted())
throw new InterruptedException();
//至少会执行一次tryAcquireShared,这是一个模板方法,供子类重写。若返回正数说明获取信号量成功,函数结束。
//如果是非公平模式,最终会调用nonfairTryAcquireShared方法,再下文讲解;
//如果是公平模式,最终会调用FairSync的tryAcquireShared方法,下文详解。
if (tryAcquireShared(arg) < 0)
//如果尝试获取信号量失败,就会入队
doAcquireSharedInterruptibly(arg);
}
我们首先看一下非公平模式下,nonfairTryAcquireShared的实现,这个比较容易理解。
final int nonfairTryAcquireShared(int acquires) {
//一个死循环
for (;;) {
//获得state的当前值。记得上文说过,state代表了目前可用的信号量总数。
int available = getState();
int remaining = available - acquires;
//remaining代表减去acquires之后,还剩下信号量的数目。
//如果剩下数目小于0,说明当前的信号量不够,此时return remainging(负数),意味着尝试获取失败
//如果remaining>=0,CAS设置state值为remaining,若CAS成功,return 正数,代表尝试获取信号量成功
//可以看到,若信号量数目够用,且一直CAS失败,就会一直循环下去
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
在看一下公平模式下tryAcquireShared的实现。可以看到,相比较于非公平模式,公平模式首先会检查当前线程是否需要排队。若需要排队代表此次尝试获取信号量失败,要进队列排队。若不需要排队,后面的逻辑和上文的nonfairTryAcquireShared是一样的。
protected int tryAcquireShared(int acquires) {
for (;;) {
//hasQueuedPredecessors,检查当前线程是否需要排队。
//这个方法短小精悍,却十分重要,在JUC多出都有应用,下面会详细分析。
//若返回true,则直接返回-1,此次尝试获取信号量失败
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
下面这个方法是用来判断当前线程是否需要入队列排队。在详细剖析之前,首先申明一点:
队列头节点是不参与排队的,队列中的第二个节点是第一个参与排队的节点。
类比买火车票,第一个人正在买票,他能算是排队吗?显然不能。他后面的那个人才是第一个排队的人。
因此,你记住,在AQS里,头结点永远不参与排队。这点很重要。
public final boolean hasQueuedPredecessors() {
//队列尾节点
Node t = tail;
//队列头结点
Node h = head;
Node s;
//返回值分情况讨论:
//1. head和tail都是null。此时h!=t 是false。方法直接返回false。代表不需要排队。
//这说明此前尝试获取信号量时,信号量一直够用,且每次CAS扣减信号量都成功。参考tryAcquireShared
//head和tail不可能一个是null,一个不是null,那么接下来就讨论两者都不是null的情况。
//2. head和tail都不是null,且h==t。也就是队列中只有一个节点,它既是head也是tail。
//只有一个节点是什么情况呢?第一个线程来排队的时候,会新建一个虚拟节点作为head,
//自己所属的Node节点跟在后面,所以会有两个节点。只有一个节点是这种情况:
//某个时刻队列中有n个节点在排队,后来陆续地n个节点都获得了所需信号量,
//此时队列中就只剩下一个head节点。head节点是不参与排队的,也就是目前队列里没有排队的线程。
//那么当前线程也不排队,先去尝试扣减state。此时,h==t,方法返回false。
//3. head和tail都不是null,且h!=t。此时h!=t 是true。(s = h.next) == null是false。
//接下来看这个条件:s.thread != Thread.currentThread()。s.thread代表第一个排队的线程
//如果当前线程不是第一个排队的线程,那么没啥说的,在你之前有线程排队,你就乖乖到队列后面排队吧。返回true
//如果当前线程是第一个排队的线程,那么 s.thread != Thread.currentThread()是false。方法整体返回false
//这种情况不太明白,当前线程如果正在排队,它应该是被park了才对,怎么会又一次来排队呢?
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
以上,分析了尝试获取信号量,若尝试失败,则执行下面的方法。看字面含义是:
以共享模式可中断地获取信号量。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//addWaiter方法向队列中新增一个共享模式的Node节点,源码在下面分析
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//拿到node的前置节点p
final Node p = node.predecessor();
//如果前直接点是head,也就是说自己是第一个排队的线程。
if (p == head) {
//再次尝试获取信号量。有人会说,前面尝试过一次,失败了才来入队的,在尝试一次是不是浪费?
//非也。上一次尝试失败了,现在进入队列去排队了,发现自己竟然是第一个排队的,再去尝试一次很合理。
int r = tryAcquireShared(arg);
//r >=0说明尝试获取信号量成功。当然,几率很小。
//如果成功了,就把自己设置为头结点head。这也印证了上文的分析。队列中只有一个节点的情况,
//就是对应于原来所有排队的线程都成功获得了信号量。而这唯一的节点就是原先的最后一个排队的节点。
if (r >= 0) {
//
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//如果不是第一个排队的或者尝试获取信号量失败,就走到了这里,既然要排队,当前线程就要park(沉睡)了。
//但是自己什么时候能醒来呢?所以在沉睡前,先把前置节点的waitStatus置为SIGNAL(-1),
//意为,当前置节点成功获取到信号量,退出队列的时候叫醒自己。
//shouldParkAfterFailedAcquire就是把前置节点状态设置为SIGNAL
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
//如果执行到这里。说明当前线程在排队过程中被中断了。那么终止当前获取行为。
cancelAcquire(node);
}
}
addWaiter方法,增加新的节点至排队队列中。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//此处检查tail是不是null,其实就是检查队列有没有初始化。这两者是等价的。
//前面分析过,只要初始化过,队列至少会有一个节点,tail不会是null。
//若tail不是null,那么CAS把当前节点追加至tail后面,成为新的tail。
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//若tail==null,或者CAS操作失败,则执行enq,下面解析
enq(node);
return node;
}
private Node enq(final Node node) {
//死循环
for (;;) {
Node t = tail;
//如果tail是null,说明未初始化队列,则新建一个节点,head和tail都 指向它。
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//若tail不为空,则CAS设置node为新的tail,直至成功。因为这在一个死循环里。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
至此,Semaphore的acquire(n)方法解析完毕。
public final boolean releaseShared(int arg) {
//CAS尝试修改state变量的值
if (tryReleaseShared(arg)) {
//释放队列中的相应节点
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果当前节点的ws是SIGNAL,代表它有义务叫醒后续节点
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//叫醒后续节点,下文做详细分析
unparkSuccessor(h);
}
//目前没有领悟到设置ws为PROPAGATE有什么用
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//英文注释说的很明白了。我有一点不明,为什么要从尾节点倒叙遍历?
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}