Semaphore实现原理
构造源码
//默认是非公平
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
Semaphore semaphore=new Semaphore(2);
当调用new Semaphore(2) 方法时,会把初始令牌数量赋值给同步队列的state状态,state的值就代表当前所剩余的令牌数量。
初始化完成后同步队列信息如下图:即头尾是空,指针指向null
获取令牌源码
semaphore.acquire();
1.当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取到一个令牌则修改state=state-1。
2.当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。
3.当计算出来的state>=0,则代表获取令牌成功,如果是大于等于0说明获取令牌成功。
//默认获取1个令牌
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//共享模式下获取令牌,获取成功则返回,失败则加入阻塞队列,挂起线程
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取令牌,arg为需要获取令牌个数 默认arg=1
//tryAcquireShared有2种实现:公平&&非公平
//tryAcquireShared的返回值是剩余令牌数
if (tryAcquireShared(arg) < 0){
//当(可用令牌数) 减 (需要获取令牌数) 小于0 即 剩余令牌数小于0
//创建一个节点加入阻塞队列
//如果当前节点是头结点的下一个节点,则会尝试获取锁,获取失败挂起当前线程。
//如果当前节点的不是头结点的下一个节点,则挂起当前线程。
doAcquireSharedInterruptibly(arg);
}
}
tryAcquireShared:非公平锁:由令牌数决定是否获取锁
来了就去占有锁,然后返回剩余的令牌数。如果剩余令牌数小于0,说明获取锁失败需要阻塞在等待队列。
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
//如果可用令牌数 - 待获取的令牌数 小于0 直接返回剩余令牌数
//如果可用令牌数 - 待获取的令牌数 大于0 设置剩余令牌数到state中
//此处cas可能会失败,但是因为死循环的存在,所以cas失败会继续重试
//这里也有可能获取不到
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
}
tryAcquireShared:公平锁:由等待队列决定是否获取锁
如果等待队列存在数据,那么直接返回-1,代表获取锁失败直接挂起。
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;
}
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
doAcquireSharedInterruptibly
第一种情况:当前节点头结点的下一个节点即当前节点是老二,那么尝试获取令牌:
如果获取令牌成功并且剩余令牌数大于等于0,那么执行setHeadAndPropagate方法唤醒等待队列节点并返回。
如果获取令牌失败那么进入shouldParkAfterFailedAcquire方法中然后陷入阻塞
问题:为什么剩余令牌数等于0也要唤醒节点?
等于0说明正好state=1即有1个令牌,然后当前线程获取到了这个令牌,剩余令牌数是0。
在setHeadAndPropagate方法中 除了判断剩余令牌数还判断了head的waitStatus,所以在这个方法里并不一定会唤醒节点。
完整判断:propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0)
第二种情况:当前节点不是老二,进入shouldParkAfterFailedAcquire方法,在shouldParkAfterFailedAcquire方法中:
先判断当前节点的前置节点的waitStatus是为-1:如果是那么直接返回true陷入阻塞。
再判断当前节点的前置节点的waitStatus是否大于0:
如果是大于0:说明前置节点是取消状态,会断开前置节点,再次进入循环。
如果不大于0:那么修改当前节点的前置节点的waitStatus=-1再次进入循环:判断当前节点是否是老二:
如果是就再尝试加一次锁,走第一种情况。
如果不是:进入shouldParkAfterFailedAcquire,判断当前节点的前置节点的waitStatus=-1,进入阻塞。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//创建节点加入阻塞队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
//注意这里是个死循环
//注意这里是个死循环
//注意这里是个死循环
for (;;) {
//获得当前节点的pre节点
final Node p = node.predecessor();
//如果是当前节点的pre节点是头结点
//说明自己是老二
if (p == head) {
//再次尝试获取锁
//首先获取剩余令牌数
int r = tryAcquireShared(arg);
//如果剩余令牌数r>=0 说明有可用的令牌
//为什么等于0也要唤醒呢?
//在setHeadAndPropagate方法中 除了判断剩余令牌数还判断了head的waitStatus。
//完整判断:propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0)
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);
}
}
shouldParkAfterFailedAcquire
//pred是node的前置节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//对于没有获取到锁的节点是true 会被阻塞
if (ws == Node.SIGNAL)
return true;
//如果ws大于0 断开节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果ws小于0 修改ws为-1
//在下一轮循环返回true
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回false说明不需要park
return false;
}
parkAndCheckInterrupt
节点被park在这里
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
setHeadAndPropagate
//node是当前节点的前置节点即头结点 propagate是剩余可用令牌数
//为什么叫setHeadAndPropagate?
//setHead我们看到了,那么propagate如何理解?
//setHeadAndPropagate()方法就是在一个线程获取到令牌之后,唤醒它之后排队获取令牌的线程的。
//该方法可以保证线程2获取令牌后,唤醒线程3获取令牌,线程3获取令牌后,唤醒线程4获取令牌,以此类推,直到所有的线程都获取一遍令牌。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//设置当前节点为头结点
setHead(node);
//如果剩余可用令牌数大于0 或者 头结点不为空 或者头结点等待状态<0
//剩余可用令牌数大于0 说明有资源可以争抢 可以唤醒等待节点来获取
//头结点为空 或者头结点等待状态<0 说明还有节点在等待 需要唤醒等待节点来获取
if (propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
//唤醒等待节点
doReleaseShared();
}
}
//解释了为什么Thread-0被设置为头结点后thread是null
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
doReleaseShared
private void doReleaseShared() {
//注意这里是个死循环
for (;;) {
//每次循环都会获取新头
Node h = head;
if (h != null && h != tail) {
//获取头结点等待状态。
int ws = h.waitStatus;
//是否需要唤醒后继节点:如果头结点等待状态为 Node.SIGNAL
if (ws == Node.SIGNAL) {
//修改头结点状态为初始状态0。
//为什么要修改为0?
//为什么要修改为0?
//为什么要修改为0?
//因为shouldParkAfterFailedAcquire会判断前置节点的waitStatus 只有waitStatus<=0才会尝试获取1次锁
//当线程获取到了锁会修改头结点的状态为-1
//compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
//假设并发执行下面的cas会发生什么?即2个锁同时释放然后执行doReleaseShared
//这个问题答案在最后
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//修改失败 继续循环
continue;
//唤醒头结点的下一个节点线程
unparkSuccessor(h);
}else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE));
}
// loop if head changed:如果头部改变就继续循环
// 什么时候头发生变化?
// 其他的线程获取到了令牌,调用setHeadAndPropagate
if (h == head)
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);
}
释放令牌
semaphore.release();
当调用semaphore.release() 方法时
1、线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程
2、释放令牌成功之后,同时会唤醒同步队列的所有阻塞节共享节点线程
3、被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程。
源码:
release
//释放令牌
public void release() {
sync.releaseShared(1);
}
releaseShared
//释放共享锁,同时唤醒阻塞队列共享节点线程
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;
}
}
doReleaseShared
// 唤醒共享节点线程
private void doReleaseShared() {
//注意这里是个死循环 释放所有的等待节点
for (;;) {
Node h = head;
if (h != null && h != tail) {
//获取头结点等待状态
int ws = h.waitStatus;
//是否需要唤醒后继节点
if (ws == Node.SIGNAL) {
//修改状态为初始状态0
//为什么要修改为0?
//因为shouldParkAfterFailedAcquire会判断前置节点的waitStatus 只有waitStatus<=0才会尝试获取1次锁
//为什么会修改失败?
//修改失败说明其他线程已经修改了
//其他线程在哪里修改的?
//在unparkSuccessor方法
//思考:
//假设头结点的ws == Node.SIGNAL,线程1 和 线程2 同时执行到了这里
//线程1 执行 compareAndSetWaitStatus 成功 waitStatus由 -2 ---> 0
//线程2 执行 compareAndSetWaitStatus 失败
//线程1 执行 unparkSuccessor(h) 唤醒头结点的下一个节点线程
//线程2 重新进入循环,执行 ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
//ws==0为true 并且 compareAndSetWaitStatus(h, 0, Node.PROPAGATE) 也为true
//修改为-3的意义在哪呢?
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)){
continue;
}else{
//唤醒头结点的下一个节点线程
unparkSuccessor(h);
}
//如果其他线程设置为0,那么我们要设置状态为PROPAGATE = -3
//为什么设置为PROPAGATE??
}else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE));
}
// loop if head changed:循环到头发生变化
// 什么时候头发生变化? 其他的线程获取到了令牌
if (h == head)
break;
}
}
//node是头结点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//头结点的ws < 0,cas修改ws为0 和上面的 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 呼应
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);
}
节点被唤醒后的流程
首先思考:被唤醒的节点阻塞在哪了?
阻塞在doAcquireSharedInterruptibly方法中
再思考:被唤醒的节点接下来会做什么?
1.获得当前节点的pre节点 2.如果当前节点的pre节点是头结点说明自己是老二 3.再次尝试获取锁 4.如果获取锁成功,首先获取剩余令牌数 5.如果剩余令牌数大于0 说明有可用的许可量 6.设置自己为头结点并唤醒等待队列上的节点
注意被唤醒的节点不一定会竞争锁,,等待队列中被阻塞的节点会在死循环里被唤醒,然后重新进入进入死循环,判断自己是不是老二,如果不是老二,继续去唤醒。
/**
* 1、创建节点,加入阻塞队列,
* 2、重双向链表的head,tail节点关系,清空无效节点
* 3、挂起当前节点线程
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//创建节点加入阻塞队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
//注意这里是个死循环
//注意这里是个死循环
//注意这里是个死循环
for (;;) {
//获得当前节点的pre节点
final Node p = node.predecessor();
//如果是当前节点的pre节点是头结点
//判断自己是不是老二,如果是那么就尝试获取资源
if (p == head) {
//再次尝试获取锁
//首先获取剩余令牌数
int r = tryAcquireShared(arg);
//如果剩余令牌数大于等于0
if (r >= 0) {
//如果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);
}
}
shouldParkAfterFailedAcquire
//pred是node的前置节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//对于没有获取到锁的节点是true 会被阻塞
if (ws == Node.SIGNAL)
return true;
//如果ws大于0 断开节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果ws小于0 修改ws为-1
//在下一轮循环返回true
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//对于获取到锁的节点是false ,因为获取到锁的时候会设置头结点状态为0
return false;
}
setHeadAndPropagate
第一种情况:当前节点是老二,那么尝试获取令牌如果已经获取到令牌,那么执行唤醒等待队列节点的操作
//node是当前节点的前置节点即头结点 propagate是剩余可用令牌数
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//设置当前节点为头结点
setHead(node);
//如果剩余可用令牌数大于0
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
//唤醒等待节点
doReleaseShared();
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
继上面的图,当我们线程1调用semaphore.release(); 时候整个流程如下图:
总结
加锁:
1.当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取一个令牌则修改为state=state-1。
2.当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。
3.当计算出来的state>=0,则代表获取令牌成功,如果是大于0说明还有剩余的资源会唤醒阻塞队列的节点。
释放锁:
1.线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程
2.释放令牌成功之后,同时会死循环唤醒同步队列的所有阻塞节共享节点线程。
3.被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取令牌成功唤醒其他节点,否则重新进入阻塞队列,挂起线程。
哪些地方调用了doReleaseShared
doReleaseShared的作用是唤醒等待队列上的节点。
1.加锁的时候,加锁成功且剩余令牌数大于等于0:调用setHeadAndPropagate。
2.解锁的时候, 唤醒同步队列上的线程,如果加锁成功且剩余令牌数大于等于0:调用setHeadAndPropagate。
为什么叫setHeadAndPropagate
代码案例1
private static Semaphore semaphore = new Semaphore(1);
public static void main(String[] args) {
//模拟5辆车进入停车场
for (int i = 0; i <5 ; i++) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("===="+Thread.currentThread().getName()+"来到停车场");
//获取令牌尝试进入停车场
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"成功进入停车场");
//模拟车辆在停车场停留的时间
Thread.sleep(new Random().nextInt(10000));
System.out.println(Thread.currentThread().getName()+"驶出停车场");
// semaphore.release()在一个线程多次获取后必须多次释放
// 释放令牌,腾出停车场车位
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},i+"号车");
thread.start();
}
}
====0号车来到停车场
====3号车来到停车场
====2号车来到停车场
====1号车来到停车场
0号车成功进入停车场
====4号车来到停车场
0号车驶出停车场
3号车成功进入停车场
3号车驶出停车场
2号车成功进入停车场
2号车驶出停车场
1号车成功进入停车场
1号车驶出停车场
4号车成功进入停车场
4号车驶出停车场
0号车相当于是第一个抢占到令牌的线程,直接进入了停车场。
当0号车释放资源,通知4号车,4号车进入停车场,以此类推。
当前我们只有1个资源
Semaphore semaphore = new Semaphore(1);
假设我们有2个资源呢?必然有2个线程先获取到资源,这2个线程释放资源以后,都会通知其他的线程。
代码案例2
在获取令牌的doReleaseShared方法中我们提出了1个问题, 假设cas并发修改头部的waitStatus=0会发生什么?
即2个锁同时释放然后执行doReleaseShared方法中的这一句代码:if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))。
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;
}
}
//假设线程1 成功执行cas 然后执行 unparkSuccessor方法,即线程1释放锁成功并唤醒线程3,此时线程2cas必然执行失败重新进入死循环
//线程3会判断自己是否是老二,然后获取令牌成功后执行setHeadAndPropagate方法
//如果进入死循环时 线程3还没执行 setHeadAndPropagate方法 即头还没改变
//线程2cas仍会执行失败重新进入死循环
//如果进入死循环时 线程3已经执行 setHeadAndPropagate方法 即头已经改变为线程3
//线程2cas会执行成功
这说明了,即使多个线程同时释放令牌,这些线程也要通过cas的方式一个一个抢占式地去唤醒同步队列上的节点。
只有同步队列上的节点获取锁成功并且设置自己为头部,后续的线程才可能cas成功从而唤醒下一个节点。
模拟cas失败的场景
static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
for (int i = 0; i <2 ; i++) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("===="+Thread.currentThread().getName()+"来到停车场");
//获取令牌尝试进入停车场 先让t0和t1获取到锁
semaphore.acquire();
//睡眠5s 让线程2 3 4 入队
Thread.sleep(1000 * 5);
System.out.println(Thread.currentThread().getName()+"成功进入停车场");
//模拟车辆在停车场停留的时间
System.out.println(Thread.currentThread().getName()+"驶出停车场");
// semaphore.release()在一个线程多次获取后必须多次释放
// 释放令牌,腾出停车场车位
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},i+"号车");
thread.start();
}
for (int i = 2; i <5 ; i++) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
//睡眠3s让 t0 t1 有足够的时间获取到锁
Thread.sleep(1000 * 5);
System.out.println("===="+Thread.currentThread().getName()+"来到停车场");
//加入队列
semaphore.acquire();
//再睡1分钟 让t0 和 t1 同时释放锁 观察结果
Thread.sleep(10000 * 60);
System.out.println(Thread.currentThread().getName()+"成功进入停车场");
System.out.println(Thread.currentThread().getName()+"驶出停车场");
// semaphore.release()在一个线程多次获取后必须多次释放
// 释放令牌,腾出停车场车位
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},i+"号车");
thread.start();
}
}
多线程打断点即可。
为什么设置等待状态为PROPAGATE?
为什么引入PROPAGETE,是为了解决JAVA6之前的一个bug。
可以想象这样一种场景:假如当前CLH队列中有一个空节点和两个被阻塞的节点(t1和t2想要获取信号量但获取不到被阻塞在CLH队列中:(state初始为0))
head(ws=0) --------> t1(ws=-1)-------->t2(ws=-1)-------->tail(t3,ws=-1),t4和t5是已经获取到锁的线程。
在JAVA6的时候,当时的代码如下
private void setHeadAndPropagate(Node node, int propagate) {
//将当前节点设置为头节点
setHead(node);
if (propagate > 0 && h.waitStatus != 0 ) {
Node s = node.next;
//获取当前节点的后继节点,如果它为null或者它是共享节点,则唤醒头节点的后继节点
if (s == null || s.isShared()) {
//唤醒后继节点
doReleaseShared();
}
}
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) {
//头节点不为空,且状态不为0时,调用唤醒节点方法
unparkSuccessor(h);
}
return true;
}
return false;
}
时刻1:t4调用release->releaseShared->tryReleaseShared,将state+1变为1。在doReleaseShared方法中发现此时的head节点不为null并且waitStatus为-1,,会用cas将head的waitStatus改为0。然后调用unparkSuccessor方法唤醒t1。
时刻2:t1被上面t4调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时t1还没有调用setHeadAndPropagate方法。
时刻3:t5调用release->releaseShared->tryReleaseShared,将state+1变为1,同时发现此时的head节点虽然不为null,但是waitStatus为0,所以就不会执行unparkSuccessor方法。
时刻4:t1执行setHeadAndPropagate->setHead,将头节点置为自己。但在此时propagate也就是剩余的state已经为0了(propagate是在时刻2时通过传参的方式传进来的,那个时候-1后剩余的state是0),即propagate > 0 条件不满足。所以也不会执行unparkSuccessor方法。
至此可以发现一轮循环走完后,CLH队列中的t2线程永远不会被唤醒,主线程也就永远处在阻塞中,这里也就出现了bug。
其实对于线程1来说确实是可以不往下传播往下继续唤醒节点,但是线程4肯定是要往下继续传播唤醒节点的。
那么来看一下现在的AQS代码在引入了PROPAGATE状态后,在面对同样的场景下是如何解决这个bug的:
private void setHeadAndPropagate(Node node, int propagate) {
//获取队列头节点
Node h = head;
//将当前节点设置为头节点
setHead(node);
//如果propagate>0表示该资源还可以被获取
//如果旧头节点为null或者旧头节点的状态小于0
//如果新头节点为null或者新头节点的状态小于0
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//获取当前节点的后继节点,如果它为null或者它是共享节点,则唤醒头节点的后继节点
//读读共享,读写互斥,写写互斥
if (s == null || s.isShared()) {
//唤醒后继节点
doReleaseShared();
}
}
}
private void doReleaseShared() {
for (; ; ) {
Node h = head;
if (h != null && h != tail) {
//如果头节点不为空,且存在后继节点
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//判断头节点状态是否为-1,如果不是,则继续循环
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
//判断头节点的状态是否为-1,不是的话,就等待下次循环
continue;
}
//如果头节点状态为-1,则将它更改为0,再来唤醒后继节点
unparkSuccessor(h);
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
//如果头节点状态为0,且未能将该节点的状态更改为-3,则继续下一次循环
continue;
}
}
if (h == head) {
//如果头节点还未发生变化,则跳出循环
break;
}
}
}
时刻1:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,继续调用doReleaseShared方法,将head的waitStatus改为0,同时调用unparkSuccessor方法。
时刻2:t1被上面t4调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。注意,此时t1还没有调用的setHeadAndPropagate方法。
时刻3:t5调用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状态后,可以有效避免在高并发场景下可能出现的、线程没有被成功唤醒的情况出现。
继续思考上面的场景如果没有时刻3,执行流程会是什么样子的?
时刻1:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,继续调用doReleaseShared方法,将head的waitStatus改为0,同时调用unparkSuccessor方法。
时刻2:t1被上面t4调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。
时刻4:t1执行setHeadAndPropagate->setHead,将新的head节点置为自己。虽然此时propagate依旧是0,但是“h.waitStatus < 0”这个条件是满足的(h现在是SIGNAL状态),同时下一个节点也就是t2也是共享节点,所以会执行doReleaseShared方法,将新的head节点(t1)的waitStatus改为0,同时调用unparkSuccessor方法,此时也就会唤醒t2了。
继续思考一种极端的场景:t1执行完setHeadAndPropagate的setHead方法后的一瞬间,被其他线程把状态由-1改为了0。
时刻1:t4调用release->releaseShared->tryReleaseShared,将state+1变为1,继续调用doReleaseShared方法,将head的waitStatus改为0,同时调用unparkSuccessor方法。
时刻2:t1被上面t3调用的unparkSuccessor方法所唤醒,调用了tryAcquireShared,将state-1又变为了0。
时刻3:t1执行setHeadAndPropagate->setHead,将新的head节点置为自己。此时t4修改了头的状态即t1的状态由-1变为0。
时刻4:t1发现 (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) 条件不满足
因为propagate==0&& h.waitStatus==0
时刻5:t4会调用unparkSuccessor(h);唤醒t2