在java代码中,代码为了保证逻辑的原子性,往往会给代码加锁,防止多线程并发下对非原子性操作的执行,造成逻辑紊乱。
aqs是由Doug Lee写的对于synchronized的优化,aql是clh锁,即Craig, Landin, and Hagersten (CLH),CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,线程只需要在本地自旋,查询前驱节点的状态,如果前驱节点释放了锁,就结束自旋。
在aqs中,维护了一个volatile int state(共享资源)和一个FIFO的线程等待队列,线程在获取锁失败之后,会放置进入等待队列的对尾,知道被前驱节点唤醒。
能够访问state的方式有三种:
getState、setState、compareAndSetState
在aqs中有两种模式:独占模式(Exclusive):只有一个线程能执行
共享模式(Share):可以多个线程同时执行
aqs使用了模版方法的设计模式,在类中定义好了队列的维护算法。将state该共享资源的获取与释放留给实现者去实现,主要是以下几个方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
锁的释放与获取没有定义成abstract,所以可以继承的时候,只实现一种模式
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可
AQS是双向链表的形式保存每个等待的任务,在Node中存在属性waitStatus,有5种值:
-
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
-
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
-
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
-
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
-
0:新结点入队时的默认状态。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
独占模式锁的入口:acquire(int)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1、tryAcquire尝试获取共享资源,如果获取到了,返回true否则false(非公平,每个线程进来会直接抢夺共享资源,失败才去CLH队尾)
2、addWaiter失败后,将任务添加到等待队列的队尾,并标记为独占模式
3、acquireQueued 线程阻塞在队列中获取资源,一直到获取到资源之后才返回,如果在等待过程中被中断过,则返回true,否则返回false
4、如果线程在等待的过程当中被中断过,它是不响应该中断的。只是获取资源之后再进行自我中断selfInterrupt ,补上中断
独占模式:
acquire(int)获取锁之后,如果获取失败,则会放进等待队列,直到前驱节点唤醒。
共享模式:前驱节点被唤醒后,state如果资源还有剩余,会继续唤醒后续节点
waitStatus之PROPAGATE(-3) 状态
在jdk1.6早期一些版本中,是没有PROPAGATE这个状态的,这个状态是为了解决bug,即线程阻塞,永远不会被唤醒的状态;
PROPAGATE只在 doReleaseShared 方法中被使用:
出现的bug(共享模式):
举例,如果有四条线程,t1,t2获取锁,在CLH中阻塞,t3、t4的线程只执行release方法
即此时aqs中 head->t1->t2 state状态尾-1(SIGNAL)
1、线程t3,调用releaseShare方法,在tryReleaseShare中state+1 设置为1了,head非空,并且waitStatus=-1,于是调用unparkSuccessor唤醒后续线程,并将head的waitStatus设置为0
2、线程t1,被t3使用unparkSuccessor方法唤醒了,调用tryAcquireShared将state-1又变成了0,此时,调用链还没走到setHeadAndPropagate
3、线程t4,调用了releaseShare方法,调用tryAcquireShared将state+1,变成了1,head非空,并且waitStatus为0,所以不会执行unparkSuccessor方法
4、线程t3,完成 int r = tryAcquireShared(arg);方法后,进入setHeadAndPropagate方法,将自己设置为head,但在此时propagate也就是剩余的state已经为0了(propagate是在步骤2时通过传参的方式传进来的,那个时候-1后剩余的state是0),所以也不会执行unparkSuccessor方法。
至此可以发现一轮循环走完后,CLH队列中的t2线程永远不会被唤醒,主线程也就永远处在阻塞中,这里也就出现了bug。那么来看一下现在的AQS代码在引入了PROPAGATE状态后,在面对同样的场景下是如何解决这个bug的:
1、线程t3,调用releaseShare方法,在tryReleaseShare中state+1 设置为1了,head非空,并且waitStatus=-1,于是调用unparkSuccessor唤醒后续线程,并将head的waitStatus设置为0
2、线程t1,被t3使用unparkSuccessor方法唤醒了,调用tryAcquireShared将state-1又变成了0,此时,调用链还没走到setHeadAndPropagate
3、线程t4,调用了releaseShare方法,调用tryAcquireShared将state+1,变成了1,head非空,并且waitStatus为0,所以不会执行unparkSuccessor方法,但是!!!会进入另一个if,将waitStatus从0改为PROPAGATE状态。
4、线程t3,完成 int r = tryAcquireShared(arg);方法后,进入setHeadAndPropagate方法,将自己设置为新head,但在此时propagate也就是剩余的state已经为0了(propagate是在步骤2时通过传参的方式传进来的,那个时候-1后剩余的state是0),但是,在setHeadAndPropagate方法中,获取判断原本head的waitStatus<0,此时waitStatus为-3,满足条件,且t1后一个节点t2非空,为共享节点,所以调用doReleaseShared方法,唤醒新的头节点后面的节点,即线程t2
条件队列:独占模式下,在调用await方法的时候,会将节点放置到condition队列当中,并释放锁,唤醒下一个等待线程,并将节点退出同步队列。挂起线程。
在调用signal方法的时候,会恢复条件队列的第一个节点,并在条件队列中删除,之后再将该节点重新放置待同步队列当中。