参考:【死磕Java并发】-----J.U.C之AQS:AQS简介
参考:源码阅读.
文章目录
1.AQS
1.AQS作用
在讲ReentrantLock之前科普一下AQS的概念,ReentrantLock是基于AQS的方法实现了线程解锁和解锁的管理。
AQS,即队列同步器。它是构建锁或者其他同步组件的基础框架,能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。
AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。
AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
AQS大量使用Unsafe类方法实现CAS操作,关于Unsafe请参考:Java中的Unsafe
2.Node:同步队列节点
在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next):
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
//共享式节点
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
//独占式节点
static final Node EXCLUSIVE = null;
//因为超时或者中断,节点会被设置为取消状态
static final int CANCELLED = 1;
//后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,
//将会通知后继节点,使后继节点的线程得以运行
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
//前驱结点
volatile Node prev;
//后续节点
volatile Node next;
//获取同步状态的线程
volatile Thread thread;
}
2.构造方法
ReentrantLock支持两种锁,一种是非公平可重入锁,一种是公平可重入锁。非公平可重入锁可能出现一个线程长时间占有锁的情况,而公平锁对于锁的持有先对平均,正常情况下使用非公平锁,效率更高,因为减少了线程切换的消耗:
Unsafe类的说明可以参考:Java中的Unsafe
他同样是使用CAS的做法去实现设值,其中有三个变量:origin、expect、update,这边的名字可能不是很准确,我的理解是在内存中某个变量的原始值为origin,在没有修改之前,我的期望except应该是等于origin的。而最终落地到内存的值应该是update.
举个例子,本来内存中的A地址存储着0,这个时候需要把0改成1,1就是update,而0就是except.而在我们修改之前有一个线程进来把0改成了2,这个时候2不等于我们的except,0,所以修改失败了.当然底层的逻辑和实现肯定更加复杂.
3.公平与非公平
之前对于公平与非公平的概念一直没理解清除,这边希望标注一下,保证理解了这个公平的概念再往下看:
所谓公平其实是相对的,因为AQS维护了一个队列,所以对于队列中的节点一定是有序的,这个是毋庸置疑的,那么怎么实现非公平呢?就是直接越过排队,不关心队列是不是有人在排队,直接插入去尝试获取锁,获取成功就是挣到了,获取失败就进队列乖乖等待了。
所以相对而言,公平就是保持队列的完整性,如果有人在排队,不允许插队,必须乖乖排队.
4.非公平锁
1.加锁过程
因为AQS提供了一个FIFO的队列,先对完整的支持了不同情况的锁,所以直接查看AQS的方法可能会懵逼,因为一些处理是针对特殊的锁,比如说等待超时的处理,并不适用与所有继承AQS的锁.
所以这边我的思路是直接去查看ReentrantLock的方法实现.
1.加锁成功
这边的加锁:
当一个线程进来,如果通过CAS的方式修改state成功,说明他获取到了锁。如果通过CAS的方式加锁失败(返回false)表示已经有人在此之前把我的锁拿走了,这个时候我只能去等待获取锁。就进入了下面的acquire方法。持有的参数1表示,希望加锁一次.
final void lock() {
//非公平锁的Lock先通过调用Unsafe类通过CAS方式去设置status
//当status为0的时候表示无锁,大于0为有锁,并且可支持重入,后面可以看到.
if (compareAndSetState(0, 1)){
//设置拥有者为当前线程.
setExclusiveOwnerThread(Thread.currentThread());
}
else{
//如果上述的CAS修改状态(获取锁)失败了,则会尝试继续获取或者加入队列
acquire(1);
}
}
2.加锁失败
我们需要看一下这边的实现,这是AQS提供的实现方式,而ReentrantLock实现了自己的tryAcquire方法。这边大概的做法就是通过独占锁方式(当然还有共享式)尝试获取锁,失败之后加入队列(addWaiter),在队列中等待时机。
public final void acquire(int arg) {
//tryAcquire方法看实现类具体实现,这边的注释说明是 尝试以独占模式获取。
//当这个条件不满足(即返回false,尝试失败之后)先把节点加入到队列,然后再调用acquireQueued
//如果acquireQueued返回证明这个节点是被中断唤醒,并且之前的尝试独占锁的获取失败了,此时应该声明需要中断.
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
3.尝试独占式获取
看一下tryAcquire在ReentrantLock中的实现:
只有在两种情况下返回true(竞争锁成功):
1、通过CAS方式获取锁成功(前一个获取锁的操作正好完成)
2、获取锁的线程就是锁的持有者,这个就是可重入的一个设计,所以state的值应该是0和>0.
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程.
final Thread current = Thread.currentThread();
//获取状态
int c = getState();
//如果状态为0,表示为无锁状态(当前这一刻)
if (c == 0) {
//通过CAS方式设置state,设置成功则得锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//这边就是可重入的操作,因为state!=0并且拥有锁的线程为当前线程
//所以这边会给状态加上加锁操作的次数,当前线程会继续拥有该锁.
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0){
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
//如果当前线程没有竞争到锁,则返回false.
return false;
}
4.添加到队列尾部
也就是说如果不是可重入或者是CAS获取锁成功,就会执行后面的操作,先来看一下addWaiter:
第一次快速尝试看是否能通过CAS的方式把节点添加到队列的尾部,如果这一次尝试失败了,再往下会进入死循环,一直尝试把节点加入到队列,造成的结果就是说,节点一定会被加到队列的尾部
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;
//当tail引用指向的尾结点变量存在,尝试将当前节点添加到尾结点(这一步是CAS操作.)
if (compareAndSetTail(pred, node)) {
//如果成功,则设置之前尾结点的next.
pred.next = node;
return node;
}
}
//如果上面的尝试失败了,会进入这边通过死循环去尝试添加.
enq(node);
return node;
}
private Node enq(final Node node) {
//进入死循环,
for (; ; ) {
Node t = tail;
//通过CAS的方式尝试加入头结点
if (t == null) {
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
//通过CAS的方式尝试加入尾结点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
5.在队列中获取锁
当节点被加载到队列尾部的时候,就要开始执行获取锁的操作,可以想象这一步其实也应该是一个死循环,不断去尝试直到获取到锁为止。
这边可以看到循环中唯一一处返回的就是当前置节点为head的时候,当前节点通过CAS方式尝试获取锁成功.为什么说当前置节点为head的时候还需要通过CAS的方式去获取一次锁,我想这边应该就是为了应对不同锁的情况。
这边有个参数是faild,这边能看到的就是在 node.predecessor() 这一步会发生报错,因为得到的前置节点为null。而另一个参数interrupted我的理解是一个中断信号量,后面会讲到
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//进入死循环
for (; ; ) {
//拿到前置节点,如果为空会报错,否则返回前置节点.
final Node p = node.predecessor();
//如果前置节点为头结点,尝试用独占方式去获取
if (p == head && tryAcquire(arg)) {
//设置头结点.并且这边会设置failed = false,这个failed的参数应该是相对于 acquireQueued来说
//这边的false标识获取队列失败,也就是说我当前已经在执行了,不需要再加入队列,所以是false.
setHead(node);
// help GC
p.next = null;
failed = false;
return interrupted;
}
//第一步判断需要阻塞(waitStatus),第二步线程先会进入休眠状态,然后当其被唤醒的时候,判断是不是中断唤醒的.
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
//failed为true只可能是发生异常的情况,非正常退出,要对这个node节点进行取消操作.
if (failed) {
cancelAcquire(node);
}
}
}
6.是否需要休眠判断
如果没有获取到锁,程序继续往下走进入shouldParkAfterFailedAcquire
这边分几种情况:
1.当前置节点waitStatus为SIGNAL的时候,后继节点需要等待被唤醒.所以node需要休眠,返回true
2.如果waitStatus>0,表示有节点被取消,需要移出队列,这边的做法是循环去查找到非取消的节点,也就是把这部分被取消的节点抛弃掉。
3.如果不满足上述两个情况,会把前置的waiStatus置为SIGNAL,返回false,这样第二次进来的时候就是返回true,说明需要休眠了.
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//节点的waitStatus为SIGNAL,表示需要被其他线程unparking唤醒。直接返回true.
if (ws == Node.SIGNAL) {
return true;
}
if (ws > 0) {
//Non-negative values mean that a node doesn't need to signal. So, most code doesn't need to check for particular values,
// just for sign
//获取同步状态失败后判断该阶段是否应该睡眠,主要根据的是waitStatus来判断(关于waitStatus的状态意义)。\
// 如果前驱节点处于SIGNAL,唤醒状态的。表示当前节点可以安心的休眠。否则判断状态是否大于0(只有取消状态才会大于0)。
// 如果大于0说明是取消状态,队列中删除该节点,然后取到prev的前驱继续判断,直到状态不为取消,然后CAS设置节点状态为SINGAL,返回false.
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//设置waitStatus不为0的节点的next为当前节点.
pred.next = node;
} else {
//尝试把状态位修改为SIGNAL.
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
7.线程休眠
如果当前线程需要休眠,就会进入parkAndCheckInterrupt方法,这里其实调用了Unsafe的park方法,让线程进入休眠,当他下一次醒来的时候,需要判断是中断叫醒的还是unpark叫醒的。如果是中断叫醒的就会返回中断标识,上面的方法会调用selfIntercept方法,给当前线程一个中断标识。
private final boolean parkAndCheckInterrupt() {
/**
* 查看类上的注释:
* 禁用当前线程,除非许可证可用
* 如果可用会立即返回,否则处于休眠状态(注意:线程在这个地方会停止了.)
* 除非发生三种情况:
* 1.其他线程调用 #unpark方法
* 2.其他线程 中断( interrupts)该线程.
* 3.还有一种情况我也没懂(这个调用时假的,没理由.)
* 这个方法不会告知是那种方式使得线程从休眠状态恢复的.
*/
LockSupport.park(this);
/**
* 上述的情况,可能是被中断的方式唤醒该线程的休眠.
*/
return Thread.interrupted();
}
在final中,总会去判断failed是不是true,如果是true,就需要取消当前节点获取锁的操作了。
这边参考:https://www.jianshu.com/p/01f2046aab64 给出了当节点非尾结点并且下一个节点需要被唤醒的操作,如果是头结点,会尝试去唤醒下一个节点.
8.异常退出,取消锁获取
private void cancelAcquire(Node node) {
//如果节点不存在,则返回.
if (node == null)
return;
//不再关联到线程
node.thread = null;
//释放 前置节点waitStatus被设置为>0的节点.直到找到有效的节点.
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
//如果是尾结点,通过CAS方式把当前节点清除掉,并且最后一个节点的next应该指向null
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
//如果node的下一个节点需要SIGNAL,并且没有(被cancel的节点pred)不是head的后继节点.则这边会设置node的前置节点指向node的后继节点.
// 不过,还少了一步呀。将successor指向pred是谁干的?
// 是别的线程做的。当别的线程在调用cancelAcquire()或者shouldParkAfterFailedAcquire()时,会根据prev指针跳过被cancel掉的前继节点,同时,会调整其遍历过的prev指针。
//参考:https://www.jianshu.com/p/01f2046aab64
if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0) {
compareAndSetNext(pred, predNext, next);
}
} else {
//如果node的前继节点是head(node是head的后继节点)
unparkSuccessor(node);
}
// help GC
node.next = node;
}
}
9.唤醒休眠中的线程(重点)
这边unparkSuccessor方法判断下一个节点被取消了或者为null,会通过tail往前查找,我的想法是,如果正向的查找可能找到的是null,没有办法指向正确的下一个节点,而通过尾节点找到反向的第一个节点(可能前面还有正常的节点),这个时候这个节点的线程被唤醒了,如果他的前置节点为head,那就皆大欢喜,但是如果他的前置节点不为head.看到死循环循环体里面的shouldParkAfterFailedAcquire有一步是循环去除掉前置的被取消的节点,然后找到真正的节点设置为SIGNAL,并且休眠当前节点,这个时候,CPU的执行权又让给了别人,整条线就串起来了。我想这样的做法应该是有两个原因:
1.通过Head查找,可能找到null的节点,而丢失所有的后继节点
2.通过tail找到第一个不是被取消的节点,因为acquireQueued的流程可以串起来,并且他有一步是删除掉其他CANCELED节点所以这样应该是最合理的.
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
//这个方法主要是唤醒node节点的后继节点,所以这边如果node节点的waitStatus<0,设置它为0,失败也没关系.
if (ws < 0){
compareAndSetWaitStatus(node, ws, 0);
}
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
//空或者被cancel了(>0),会往前查找不是node,并且waitStatus<=0的节点
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);
}
}
10.加锁过程图解
2.解锁过程
解锁的过程相对简单,我们分两个方面来看,一个是解锁成功返回true,一个是解锁失败返回false.
public void unlock() {
/**
* 解锁操作
*/
sync.release(1);
}
public final boolean release(int arg) {
//尝试解锁成功.
if (tryRelease(arg)) {
Node h = head;
//如果Head指向的节点不为空,并且waitStatus不为0 ,可能是>0已经失效,也可能是<0.
if (h != null && h.waitStatus != 0){
//如果当前只有一个现成获取了锁,则没有必要去唤醒下一个线程.
// 因为没有下一个线程,如果是两个以上的线程获取锁,那么后一个线程会把前一个线程的waitState状态改成SIGNAL。
// 可以理解为持有信号量,需要唤醒后续的节点,所以才会调用unpark去唤醒后继节点.
unparkSuccessor(h);
}
//返回true表示解锁成功.
return true;
}
//返回false表示解锁失败.
return false;
}
1.尝试解锁
正确是用锁,因为加锁成功之后,线程独占,解锁操作应该是该独占线程独有的,所以这边看到其实解锁的过程是没有使用CAS的方式去设值的。
这个地方会判断持有锁的线程非当前线程报错,应该是声明锁的错误调用,比如说可能没有调用lock方法而直接调用unlock
同样解锁的过程也是可重入的逆向过程,因为status的值是0或者>0的正数,所以释放锁需要直到status = 0,才证明锁释放成功了。所以这边尝试释放锁的过程就是对status的设置和判断
protected final boolean tryRelease(int releases) {
//因为支持可重入,所以这边通过减的方式去获取state值.
int c = getState() - releases;
//持有者非当前线程报错.
if (Thread.currentThread() != getExclusiveOwnerThread()){
throw new IllegalMonitorStateException();
}
boolean free = false;
//如果state为0,表示当前线程不持有锁,进行释放.
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//设置state.这边可能c还不等于0,说明还是锁住的状态
setState(c);
//返回解锁结果,解锁成功true,解锁失败false.
return free;
}
2.解锁成功
看一下刚才这个解锁成功之后的操作,如果这边只有一个线程获取到锁,获取head指向null,那么完全不需要去唤醒下一个节点(因为根本没这个节点了),但是如果有下一个节点,并且state不是0(无锁),会调用unparkSuccessor去唤醒下一个线程.
if (tryRelease(arg)) {
Node h = head;
//如果Head指向的节点不为空,并且waitStatus不为0 ,可能是>0已经失效,也可能是<0.
if (h != null && h.waitStatus != 0){
//如果当前只有一个现成获取了锁,则没有必要去唤醒下一个线程.
// 因为没有下一个线程,如果是两个以上的线程获取锁,那么后一个线程会把前一个线程的waitState状态改成SIGNAL。
// 可以理解为持有信号量,需要唤醒后续的节点,所以才会调用unpark去唤醒后继节点.
unparkSuccessor(h);
}
//返回true表示解锁成功.
return true;
}
3.下一个线程的唤醒
这个和上面加锁操作的异常中唤醒是一样的代码,这边不多做解释,其实就是唤醒下一个没有被取消的节点。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//这个方法主要是唤醒node节点的后继节点,所以这边如果node节点的waitStatus<0,设置它为0,失败也没关系.
if (ws < 0){
compareAndSetWaitStatus(node, ws, 0);
}
Node s = node.next;
//空或者被cancel了(>0),会往前查找不是node,并且waitStatus<=0的节点
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);
}
}
4.解锁失败
直接返回false
5.解锁过程图解
4.公平锁
1.加锁过程
公平锁的加锁过程相对简单,需要关注的还是公平锁重写的tryAcquire方法怎么去实现公平加锁。
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
//这边的tryAcquire会判断是否有线程等待,而不是想加锁就加锁,通过这个方式来实现公平锁操作
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
1.公平尝试获取锁:tryAcquire
protected final boolean tryAcquire(int acquires) {
//先拿到当前线程和state。
final Thread current = Thread.currentThread();
int c = getState();
//如果state为0
if (c == 0) {
//如果队列中没有前置等待节点,通过CAS的方式修改state.如果成功说明得到锁
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//status不为0,如果线程就是当前持有者,state可重入加上持有锁的次数.
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果有前置节点,并且当前没有占有锁, 获取锁失败应该返回false.
return false;
}
}
2.队列是否有线程在等待:hasQueuedPredecessors
我们要关注一下hasQueuedPredecessors这个方法怎么实现公平锁的判断的。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//这边其实可以这样看:当前表达式成立,那么成立的情况就是 !(当前表达式)
//!(h != t && ((s = h.next) == null || s.thread != Thread.currentThread()))
//即h==t || (h.next !=null && h.next.thread == Thread.currentThread());这个条件是不成立的.
//当head == tail说明没有节点 || head指向的节点就是当前锁的持有者.这两个条件说明前继没有等待的节点
//而这个方法想要的是判断队列前继有等待的节点,所以就是 !的情况,就是刚才看到的.
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
分析一下这个方法,这边通过反向和正向两种方式进行分析一下:
首先是反向的分析,因为这个方法的初衷是:Queries whether any threads have been waiting to acquire longer than the current thread.
说白了就是判断队列里面有没有在等待的,我不强行越过队列的顺序(非公平锁就是这么随意),乖乖的等.
反向: ! (没有现成在等待) = 队列为空 || 当前线程正好占有锁。
正向:队列中有线程在等待 = head != taill(队列不为空) && ((s = h.next) == null || s.thread != Thread.currentThread())
情况1((s = h.next) == null ):这里要参考enq方法,入队列的时候先会初始化好pre节点,而还没有初始化好next节点,说明有节点在进队列初始化,但是没有初始化完全:
情况2(s.thread != Thread.currentThread()):锁不是被当前线程持有,说明有其他线程等待
2.解锁过程
参考上面非公平锁的解锁过程.