大白话讲并发编程之Semaphore源码,也就明白限流和降级是怎么回事了
前言
上一节分享了 ReentrantLock
这个独占锁后,那么接下来就分享 Semaphore
这个共享锁的源码,当然讲的还是公平锁。
Semaphore这个类主要作用是用来作限流和降级;先说这个类怎么用;
Semaphore semaphore = new Semaphore(3)
for (int i = 0; i < 10; i++) {
new Thread(){
@Override
public void run() {
semaphore.acquire(); //获取钥匙
//业务逻辑
Thread.sleep(2000);
//业务逻辑
semaphore.release(); //释放钥匙
}
}.start()
}
这段代码的意思是:池中共有3把钥匙,有10个线程竞争,同一时刻最多只能有3个线程能够并行执行业务逻辑,这也是为什么能够用来做限流的原因,至于降级就是 tryAcquire(...)
方法的作用了,大致意思就是你自定义一个时间,比如1秒钟,1秒钟没有获得锁,就会返回 false
,我们就能够用来作降级处理了。这里暂时不细说,有兴趣的朋友可以在评论区里留言。
acquire()
是获取钥匙的方法,每成功获取一次,钥匙就会减 1 ,钥匙没有时就会入队等待;
release()
是用来释放锁的,归还钥匙,每成功释放一次锁,钥匙就会加 1;
当然上面这两个方法也有带 int
参数的,参数为多少,每次就获取 (释放) 几把钥匙。
Semaphore
怎么用说完了,接着就该看源码了
一、先看构造器
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
Semaphore总共有两个构造器:
第一个构造器只有 一个int
参数,创建的是一个非公平锁,int
参数最终会作为 state
( AQS
中最重要的变量)的值,在这里可以简单理解为共享池中的钥匙个数。只有持有钥匙的线程才能够执行业务逻辑,池中每次被线程成功获取一次,钥匙就会减1,也就是state=state-1
。
第二个构造器当布尔值为 true
时,创建公平锁,为false时,创建非公平锁,而 int
参数和第一个构造器用法是一致的;
二、acquire()方法
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
acquire()
方法,其实就是调用 acquireSharedInterruptibly(1)
方法,这个方法先会判断该线程有没有中断信号,如果有就抛出异常;没有就判断 tryAcquireSgared()
方法有没有小于 0 ,这个方法的作用是获取钥匙,获取失败就会调用 doAcquireSharedInterruptibly()
方法。
先进入tryAcquireSgared()
方法,可以看到里面是一个自旋,想要获取钥匙成功,必须同时满足三个条件:
- 队列中没有其他节点
- 钥匙池中的钥匙减去一个后还是不小于 0
CAS
设置钥匙个数state=state-1
成功
如果条件 3 不满足就会继续循环,如果条件 1 或条件 2 不满足就会获取失败,调用 doAcquireSharedInterruptibly()
方法。
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) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
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;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//这两部就是传播行为唤醒下一个节点
Node s = node.next;
if (s != null)
LockSupport.unpark(s.thread);
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//修改成功后第二次就会调这个
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//第一次调用这个方法会执行这个语句
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
doAcquireSharedInterruptibly()
这个方法,先调用入队方法 addWaiter(Node.SHARED)
,这个方法与之前讲的 ReentrantLock
里的入队方法是一样的,区别在于这里创建的节点是个共享节点不是独占节点,这里就不细讲了。入队成功后接下来的代码又很相似,先获得当前节点的前驱节点赋值给常量 p
,判断前驱节点是不是头节点,是的话就调用上面讲的 tryAcquireShared(1)
方法,去尝试获取钥匙,如果获取成功,就把当前节点设置成头节点,并且将当前节点的 thread
和前驱指针置空,接着唤醒后驱节点(这里面的逻辑比较复杂,单靠文字描述有些难度,我就直接简化了,如果希望我详细讲的就在评论区留言),同时把前驱节点的后驱指针置空。
doAcquireSharedInterruptibly
方法的第一个 if
条件就结束了,如果前面的条件没满足,就会进入 shouldParkAfterFailedAcquire(p, node)
方法,这个方法的作用是将前驱节点的生命状态( waitStatus
)修改成 SIGNAL(-1)
,修改完后再循环一次,调用 parkAndCheckInterrupt()
方法,这个方法就会调用 LockSupport.pack(this)
进行阻塞。
三、release()方法
public void release() {
sync.releaseShared(1);
}
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;
}
}
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;
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
release()
其实就是 releaseShared()
方法,它先调用了 tryReleaseShared(int releases)
尝试去释放锁,释放锁成功就会调用 doReleaseShared()
方法。
tryReleaseShared(int releases)
方法里面有个自旋,保证锁一定能够释放成功,可以看到只要 CAS
将 state = state +1
设置成功,就说明释放成功了。
doReleaseShared()
方法里面也是个自旋,先判断头节点是否不为空并且不等于尾节点,满足就会使用 CAS
将头节点的生命状态 ( waitStatus
) 设置为初始值 (0) ,接着调用 unparkSuccessor(h)
方法
由于 doReleaseShared()
方法的 continue
已经保证了它的生命状态为 初始值(0),所以会直接进入最后一个 if
条件,唤醒头结点的后驱节点, unparkSuccessor(h)
方法也就结束了。
结尾
那么今天的要讲的内容也就分享完了,如果大家有什么疑问或者发现什么错误,欢迎在评论区留言。