文章目录
Semaphore源码解读
1.Semaphore 是什么?
Semaphore 是一个的共享锁。共享锁就是允许多个线程同时获取锁,当然与锁的数量有关。如果把锁的数量设置成1,一定程度上也可以被称为独占锁😁。同样是基于AQS一些特征来实现的共享锁,支持公平与非公平特性。
2.Semaphore 的用法
用法一: 基础用法。线程每次获取一个锁。
// 创建对象并初始化,锁的数量,此处5代表,锁的数量是5,如果每次获取一个的话,同时允许5个线程通过
Semaphore semaphore = new Semaphore(5);
// 获取锁
semaphore.acquire();
// 释放锁
semaphore.release();
用法二: 基础用法。线程每次获取多个锁。
// 创建对象并初始化,锁的数量,此处5代表,锁的数量是6,如果每次获取一个的话,同时允许6个线程通过
Semaphore semaphore = new Semaphore(6);
// 获取锁,每次获取两个锁,即每个线程必须有两个锁,才能执行。也就是锁的数量是6,同时只能过去三个线程。
semaphore.acquire(2);
// 释放锁,当然每次获取多个锁,就会释放同样的锁,不然慢慢会死锁。
semaphore.release(2);
用法三: 基础用法。线程每次获取多个锁。
// 创建对象并初始化,锁的数量,此处5代表,锁的数量是5,如果每次获取一个的话,同时允许5个线程通过
Semaphore semaphore = new Semaphore(5);
// 尝试获取锁,如果获取不到,返回false,可以走其他逻辑。降级处理会使用到。
boolean b = semaphore.tryAcquire();
// 同样是释放锁
semaphore.release();
大致用法,分为这三种,当然还有一些其他的用法,研究一下API,即可使用。
3.Semaphore的API
- Semaphore(int permits) ,Semaphore(int permits, boolean fair),构造方法,支持公平锁和非公平锁,需要指定锁的总数量。
- acquire(),acquire(int permits),无返回值获取锁,可选参数permits 表示每次线程需要获取多少锁,才能被执行。默认值为1。允许线程被中断,抛出异常走异常的逻辑。
- acquireUninterruptibly() ,acquireUninterruptibly(int permits),无返回值获取锁,可选参数permits 表示每次线程需要获取多少锁,才能被执行。默认值为1,不支持线程中断,即线程中断不做任何处理,还是按照原来的逻辑执行
- tryAcquire(),tryAcquire(long timeout, TimeUnit unit),tryAcquire(int permits),tryAcquire(int permits, long timeout, TimeUnit unit),尝试获取锁,如果获取失败,返回false,获取成功返回true。参数:permits 同上,timeout 超时等待的时间,unit 时间单位。
- release(int permits),释放锁,切记用多少锁,释放多少锁。不然会造成锁丢失。最终无锁可用,死锁。
4.Semaphore的源码解读(公平锁为例)
4.1 从构造方法入手
由此可知,最终是去设置同步器的state字段,state代表同步器的锁的数量。不明白可以看 初始AQS这边文章 至此,构造方法结束。
4.2 从semaphore.acquire(); 获取锁逻辑入手
// 参数arg=1
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 首先对线程进行判断,如果此时线程被外部中断了,直接抛出异常,外部获取异常,可以走异常逻辑。
if (Thread.interrupted())
throw new InterruptedException();
// 没有话,就尝试去获取锁
if (tryAcquireShared(arg) < 0)
// 当有线程获取锁失败,走入队逻辑。
doAcquireSharedInterruptibly(arg);
}
首先回去进行线程的中断信号判断,有中断信号直接抛出异常,外部接收异常就可以做其他的处理。
其次,会走 tryAcquireShared(arg),之后走doAcquireSharedInterruptibly()方法
图解:
4.2.1 查看tryAcquireShared();方法
查看公平锁的逻辑
// 参数acquires等于1,此处就是公平锁的尝试获取锁的逻辑
protected int tryAcquireShared(int acquires) {
// 循环加CAS,典型的自旋锁。直到锁被释放完,就会结束此方法。
for (;;) {
// 因为是公平锁,先尝试查看队列中是否有节点。
if (hasQueuedPredecessors())
// 有的话,说明锁已经被获取完了,直接返回-1。为什么返回-1,-1就代表锁没了,思考思考哦
return -1;
// 获取state状态,即锁的数量,就是构造方法放入的。
int available = getState();
// 锁数量-1,表示有一个线程获取锁了。
int remaining = available - acquires;
// 当第一个条件成功为true的时候就会返回remaining,所以state不可能为-1。用到了短路或
if (remaining < 0 ||
// compareAndSetState 保证的是state安全性,与锁的数量无关。
compareAndSetState(available, remaining))
return remaining;
}
}
图解:
4.2.2 查看doAcquireSharedInterruptibly();方法
// 参数arg=1
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 直接入队,入队后返回当前节点。入队逻辑之前讲过。可以参考之前的博客。当前节点的mode是共享。
final Node node = addWaiter(Node.SHARED);
// 失败的条件判断
boolean failed = true;
try {
// 自选锁
for (;;) {
// 获取当前节点的前置节点。
final Node p = node.predecessor();
// 如果等于头节点
if (p == head) {
// 再次尝试获取锁,
int r = tryAcquireShared(arg);
// 返回值大于等于0,表示已经获取锁
if (r >= 0) {
// 设置头和传播,看下面代码
setHeadAndPropagate(node, r);
// 把头部节点丢弃
p.next = null; // help GC
failed = false;
return;
}
}
// 如果获取不到锁。走挂此逻辑。主要是将前一个节点是成SIGNAL表示后续节点需要被唤醒。
if (shouldParkAfterFailedAcquire(p, node) &&
// 挂起当前线程,此时阻塞完成。
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 参数node为当前节点,propagate为当前剩下锁的数量。
private void setHeadAndPropagate(Node node, int propagate) {
// 因为此阶段,只会有一个节点获取锁,所以不存在安全问题。
Node h = head;
// 把当前的节点,设置成头部节点,
setHead(node);
// 当前剩下锁的数量大于0,且头部节点等于null,此处使用的短路或,第一条件成立的时候,就会执行下面逻辑,剩下的判断,健壮性判断,无需多考虑
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取当前节点后置节点。
Node s = node.next;
// 如果当前节点等于空,或者当前节点是共享节点。
if (s == null || s.isShared())
// 走这里此处是唤醒线程,后面来讲。
doReleaseShared();
}
}
图解
4.3 从semaphore.release(); 释放锁逻辑入手
解锁逻辑不分公平和非公平锁
4.3.1 tryReleaseShared();
// 参数releases等于1
protected final boolean tryReleaseShared(int releases) {
// 自选锁,
for (;;) {
// 首先获取当前的同步器的状态,表示锁的数量
int current = getState();
// 锁的数量+1
int next = current + releases;
// 健壮判断
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
// cas 设置锁的数量
if (compareAndSetState(current, next))
return true;
}
}
图解
4.3.2 doReleaseShared();唤醒线程
// 释放共享锁
private void doReleaseShared() {
// 此处是自选锁。
for (;;) {
// 获取头部节点,每次过来都会获取当前的头部节点。
Node h = head;
// 头部节点不等于null,且头部节点不等于尾部节点。因为AQS是根据节点的waitStatus判断后置节点的。
if (h != null && h != tail) {
// 获取头部的waitStatus
int ws = h.waitStatus;
// 如果头部节点的waitStatus等于Node.SIGNAL,说明后置节点需要被唤醒。
if (ws == Node.SIGNAL) {
// 如果设置失败,接着设置,一般不会
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒线程,唤醒的是头部节点的后置节点的线程,回到java.util.concurrent.locks.AbstractQueuedSynchronizer#parkAndCheckInterrupt 方法
unparkSuccessor(h);
}
// 如果头部节点不是Node.SIGNAL,说明后置节点不需要被唤醒,尝试设置成传播。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 当前头部节点没有变化,且已经唤醒成功,结束循环。
if (h == head) // loop if head changed
break;
}
}
图解:
4.Node.PROPAGATE 字段解读
Node.PROPAGATE是为了解决一个bug,后来新增的。找到一片博客。写的还不错。拷贝过来参考一下。
而可能出现bug的测试代码如下:
1 import java.util.concurrent.Semaphore;
2
3 public class TestSemaphore {
4
5 private static Semaphore sem = new Semaphore(0);
6
7 private static class Thread1 extends Thread {
8 @Override
9 public void run() {
10 sem.acquireUninterruptibly();
11 }
12 }
13
14 private static class Thread2 extends Thread {
15 @Override
16 public void run() {
17 sem.release();
18 }
19 }
20
21 public static void main(String[] args) throws InterruptedException {
22 for (int i = 0; i < 10000000; i++) {
23 Thread t1 = new Thread1();
24 Thread t2 = new Thread1();
25 Thread t3 = new Thread2();
26 Thread t4 = new Thread2();
27 t1.start();
28 t2.start();
29 t3.start();
30 t4.start();
31 t1.join();
32 t2.join();
33 t3.join();
34 t4.join();
35 System.out.println(i);
36 }
37 }
38 }
其实上面所做的操作无非就是创建了四个线程:t1和t2用于获取信号量,而t3和t4用于释放信号量,其中的10000000次for循环是为了放大出现bug的几率,join操作是为了阻塞主线程。现在就可以说出出现bug的现象了:也就是这里可能会出现线程被hang住的情况发生(遗憾的是,我并没有模拟出来这个bug)。
可以想象这样一种场景:假如说当前CLH队列中有一个空节点和两个被阻塞的节点(t1和t2想要获取信号量但获取不到被阻塞在CLH队列中(state初始为0)):head->t1->t2(tail)。
时刻1:t3调用release->releaseShared->tryReleaseShared,将state+1变为1,同时发现此时的head节点不为null并且waitStatus为-1,于是继续调用unparkSuccessor方法,在该方法中会将head的waitStatus改为0;
时刻2:t1被上面t3调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时还没有调用接下来的setHeadAndPropagate方法;
时刻3:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,同时发现此时的head节点虽然不为null,但是waitStatus为0,所以就不会执行unparkSuccessor方法;
时刻4:t1执行setHeadAndPropagate->setHead,将头节点置为自己。但在此时propagate也就是剩余的state已经为0了(propagate是在时刻2时通过传参的方式传进来的,那个时候-1后剩余的state是0),所以也不会执行unparkSuccessor方法。
至此可以发现一轮循环走完后,CLH队列中的t2线程永远不会被唤醒,主线程也就永远处在阻塞中,这里也就出现了bug。那么来看一下现在的AQS代码在引入了PROPAGATE状态后,在面对同样的场景下是如何解决这个bug的:
时刻1:t3调用release->releaseShared->tryReleaseShared,将state+1变为1,继续调用doReleaseShared方法,将head的waitStatus改为0,同时调用unparkSuccessor方法;
时刻2:t1被上面t3调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时还没有调用接下来的setHeadAndPropagate方法;
时刻3:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,同时继续调用doReleaseShared方法,此时会将head的waitStatus改为PROPAGATE;
时刻4:t1执行setHeadAndPropagate->setHead,将新的head节点置为自己。虽然此时propagate依旧是0,但是“h.waitStatus < 0”这个条件是满足的(h现在是PROPAGATE状态),同时下一个节点也就是t2也是共享节点,所以会执行doReleaseShared方法,将新的head节点(t1)的waitStatus改为0,同时调用unparkSuccessor方法,此时也就会唤醒t2了。
至此就可以看出,在引入了PROPAGATE状态后,可以有效避免在高并发场景下可能出现的、线程没有被成功唤醒的情况出现。