AQS中共享模式源码解析
一、共享锁介绍
共享锁只存在于AQS当中,指的是在同一时刻有多个线程可以同时执行。共享锁相当于就是申请了多个令牌,每有一个线程需要去执行时就会去申请一个令牌,只有含有令牌的线程才能正常执行,没有令牌的线程只能等待有令牌的线程释放后才会去获取。
二、共享锁状态
在上一节的独占锁,我们可以发现只有三个状态0,-1(SIGNAL),1(CANCELLED),在共享锁中,新增了一个状态-3(PROPAGATE),此状态只存在于共享锁当中,主要是用于解决并发问题。
三、共享锁源码解析
我们使用Semaphore来举例看下它的加锁和解锁过程
1、共享锁加锁
在Semaphore源码中,和ReentrantLock 一样存在着公平锁和非公平锁,默认是非公平锁。
public Semaphore(int permits) {
//permits是指申请的证书数量
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
FairSync(int permits) {
super(permits);
}
Sync(int permits) {
setState(permits);
}
在构造函数中我们可以看到传入参数permits,指的是申请的证书数量。 我们根据源码可以看到最终这个申请的证书数量set进了锁状态字段state中。接下来我们调用Semaphore的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);
}
在AQS中我们可以看到默认每个线程申请的证书为1,调用Semaphore实现的tryAcquireShared()方法去获取资源。
protected int tryAcquireShared(int acquires) {
//自旋,尝试获取通行证
for (;;) {
//判断当前队列中是否有线程等待,有返回true,非公平锁这儿不去判断
if (hasQueuedPredecessors())
return -1;
//获取当前锁状态
int available = getState();
//锁状态减去申请的资源数量
int remaining = available - acquires;
//小于0 说明此时锁资源已经被分配完毕
//大于等于0时,需要去CAS更新当前锁状态为减去资源的值(此处可能存在并发获取锁)
if (remaining < 0 ||
compareAndSetState(available, remaining))
//返回当前锁状态
return remaining;
}
}
如果返回资源小于1,说明此时已经没有锁资源给新来的线程分配了,需要去进行入队操作。
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);
//r>=0,说明获取资源成功
if (r >= 0) {
//将当前结点设置为头结点,并唤醒后继结点
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//从当前结点向前寻找一个状态合格的前驱结点,将状态>1的结点出队
if (shouldParkAfterFailedAcquire(p, node) &&
//挂起当前线程
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
//如果出现异常中断,将当前线程出队
if (failed)
cancelAcquire(node);
}
}
可以看到,其实共享锁的入队逻辑和独占锁差不多,唯一的区别就是获取资源成功后,不仅设置当前结点为头结点,还会将后继结点唤醒。
最核心的点就是setHeadAndPropagate()方法,其中的逻辑看起来很简单,但是确避免了线程无法被唤醒的情况。
private void setHeadAndPropagate(Node node, int propagate) {
//在这获取到当前的头结点,但是注意有可能其他资源释放时会改变头结点状态
//也就是说我们获取头结点时状态为0,获取之后有资源释放,把头结点的状态改为了-3
Node h = head;
//设置当前结点为头结点
setHead(node);
//1.剩余资源数>0
//2.头结点为null
//3.老头结点状态小于0(有资源释放改变了头结点状态)
//4.获取新头结点为null
//5.新头结点状态小于0(有资源释放改变了头结点状态或本来状态就为-1)
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();
}
}
在这个方法中最主要的就是if里面的条件,
1.首先如果资源大于0直接去唤醒后继结点
2.如果资源数为0的时候(因为资源数是线程去获取资源返回的,有可能在返回之后又有资源释放调了)。但是此时的资源数还是之前的没有更新,所以此处进行了老头结点的判断,因为有可能在上一个线程释放资源将头结点状态改为0之后,又有资源释放,将头结点状态改成了-3并且不会去唤醒后继结点,所以由这里来去替释放的线程去唤醒后继结点。
3.此时获取到了新结点状态,有一种情况会导致后继结点不会被唤醒,当此时的头结点刚好是队尾结点,并且是尾结点时,状态为0,这时突然加入了一个新结点再头结点后面,获取资源失败之后准备去修改前驱结点的状态时,有一个资源释放会去修改此时头结点的状态未0就修改为-3并不会去唤醒后继结点,但是此时资源是有空闲的,所以这里判断了当前结点的状态<0时也会去唤醒后继结点(有可能头结点状态为-1时也会去唤醒)。
private void doReleaseShared() {
//自旋
for (;;) {
//获取最新头结点
Node h = head;
//头结点不为空且不为尾结点
if (h != null && h != tail) {
//获取头结点的状态
int ws = h.waitStatus;
//如果状态为-1
if (ws == Node.SIGNAL) {
//将头结点状态修改为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒后继结点
unparkSuccessor(h);
}
//状态为0时,修改状态为-3
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//判断头结点是否变更,没有变更中断循环
if (h == head)
break;
}
}
这个方法不仅是申请到锁时会执行,释放锁时也会执行。在这个方法中,获取了当前的头结点,判断当前头结点不为空且为尾结点,说明走到if里面的逻辑时,队列肯定是至少有两个结点的。判断当前结点状态如果为-1,说明后继结点是没有被唤醒的,这时候去CAS修改头结点状态的值。这里用CAS主要是因为可能有多个线程去并发修改头结点的状态,将线程修改成0之后去唤醒后继结点,在这儿控制了多个线程去修改线程状态后唤醒。个人认为,在这儿会造成对于的unpark(虽然唤醒之后获取不到资源还是会挂起)。
2、共享锁解锁
在解锁过程中,我们调用release()方法,底层实际调用了AQS中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;
}
}
在这个方法中主要是实现了释放锁资源的逻辑,将锁状态更新成释放后的值。而释放成功后的 doReleaseShared()方法是去进行判断是否唤醒后继线程的,如果状态不为-1为0,则修改状态为-3,如果为-1就唤醒线程。
小结:
1.共享模式下的共享锁主要是用来限流等操作的,加锁就类型于获取到令牌,获取不到则进入队列等待。
2.共享模式下有一个特殊状态-3(PROPAGATE),主要是用于解决并发时,队列中线程未被唤醒问题。
3.共享模式下多个资源释放后,后继结点并不是一下都被唤醒,而是一个结点去唤醒下一个结点。