JUC之基-AQS详解
AQS
AQS是JUC学习的基石,是JUC中许多锁的底层实现机制,我们今天从ReentrantLock出发来深入源码解读AQS的设计。
AQS底层
AQS的几个重要属性:
//阻塞队列的头
private transient volatile Node head;
//阻塞队列的尾
private transient volatile Node tail;
//核心属性,代表锁的状态(可以被各种子类实现成需要的机制)
private volatile int state;
//其父类属性,代表当前持有锁的线程
private transient Thread exclusiveOwnerThread;
AQS有一个Node内部类,是将线程封装为了阻塞队列中的节点对象,几个重要属性:
//下面几个都是线程的等待状态常量
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;
整体效果类似这样
当然,这里面的state,waitStatus我们还没有介绍含义,我们下面来介绍
state
state是AQS中最核心的一个属性,代表锁的状态,一般是为0代表可加锁,不为0代表已经有线程获得了锁,在ReentrantLock底层,实现的就是,如果有线程来就尝试cas将state由0变为1,cas成功代表加锁成功,如果锁重入那么state再加一即可。
waitStatus
waitStatus有5个值,分别是CANCELLED、SIGNAL、CONDITION、PROPAGATE和INITIAL,其中INITIAL实际上就是0,这里我加了状态INITIAL加以区分,真正源码中是没有这个状态的。我们今天要说的ReentrantLock只用到了SIGNAL和INITIAL状态,实际上,在阻塞队列中waitStatus为SIGNAL的Node有义务唤醒其后面的结点(这句话也许有点抽象,我们这里先给出一个最终阻塞队列中结点该有的状态)
事实上,当Thread-0抢到锁后,state变为1,Thread-1和Thread-2进入阻塞队列,阻塞队列会加一个Dummy结点(图中第一个线程为null的结点),这正对应我们刚才说的,“waitStatus为SIGNAL的Node有义务唤醒其后面的结点”,也就是必须有一个Dummy结点来唤醒阻塞队列中的Thread-1结点,而最后一个加入阻塞队列的Thread-2的waitStatus是INITIAL,因为他没有后一个结点,他的状态会在后一个结点插入队列中后被赋为SIGNAL
AQS行为
AQS作为一个锁框架,应该提供什么行为?要实现一个锁,需要实现哪些方法?我们应该考虑:
- 获得锁的策略?如何获得锁?
- 获得锁的线程将来如何释放锁?
- 获得不到锁的线程如何阻塞?
-
被唤醒的线程如何重新参与竞争锁?
在AQS中,这几个问题都有了实现方案:public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
线程首先通过tryAcquire()尝试获得锁,如果获得锁失败,通过acquireQueued(addWaiter(Node.EXCLUSIVE), arg)加入阻塞队列并park,而需要用户重写的是tryAcquire方法,即由用户来决定获取锁的方式,包括是否公平,是否支持重入锁等等,而获取不到锁的,AQS一律将其加入阻塞队列,并且AQS已经实现了阻塞的策略以及唤醒策略和唤醒后的重新抢占策略,并且对中断有了很好的处理。
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
对于释放锁,其先调用tryRelease()方法去尝试释放锁,这个方法由子类去实现,如果释放锁成功,就由该线程去唤醒在阻塞队列中他的下一个结点并将自己从阻塞队列中移除(也就是说,他只有唤醒了自己的下一个结点才会出队列,而不是说获取到锁就立刻出队列)
ReentrantLock运行过程
下面我们通过ReentrantLock来对AQS进行进一步介绍
- 默认非公平实现,new NonfairSync对象
-
调用lock方法,通过cas加锁(将state值从0变为1)
如果加锁成功,代表获得锁,设置AQS中Owner为当前线程(Owner有什么用?)
如果加锁失败,进入acquire(1)final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
- 调用tryAcquire(1),此处就是ReentrantLock自己的实现了,进入该方法,首先判断state是否为0,如果为0再次尝试cas,如果是重入锁,在这个方法里会返回true并将state加1
- 如果tryAcquire(1)返回false,代表获得锁失败,执行addWaiter(Node.EXCLUSIVE),此处参数代表独占锁(ReentrantLock是独占锁),这个方法的意义是将该线程加入到阻塞队列队尾等待,其中使用了经典的CAS自选volatile变量的方法
- acquireQueued真正负责了线程的阻塞,当addWaiter执行完毕后,线程已经进入队列,此时acquireQueued来对线程进行一些操作后阻塞,什么操作?还记得我们在前面说的线程等待状态吗,该方法会调用shouldParkAfterFailedAcquire方法,看当前线程在队列里的前一个线程是什么状态,如果是SIGNAL,就将当前线程park住,如果不是SIGNAL就通过cas将前一个线程的状态更新为SIGNAL,然后阻塞当前线程,也就是说,这个方法的实际作用是将该线程在队列中的前一个线程(在该线程加入队列前,这个线程是队尾,状态为INITIAL)状态改为SIGNAL并阻塞自己。这个方法还有另一个作用,我们来想,线程在这阻塞,那么将来不论是被打断还是被唤醒,首先还是在这个方法里运行,所以这个方法需要决定线程醒来之后做什么事,并且正确处理park线程被中断的情况。第一件事,处理线程醒来后做什么事,该方法规定,醒来的如果是阻塞队列中的第二个,那他就可以重新去获得锁(为什么是第二个?在下边我们通过图来介绍),如果不是第二个,就重新park。第二件事,如何处理因为中断醒来的park线程?同样的,如果是第二个,可以让其竞争锁,如果不是第二个,那就重新park。
我们来看为什么是第二个有资格去竞争锁,我们知道一开始阻塞队列中是有一个Dummy结点的,而这个结点的作用就是为了唤醒其后面的实际上的“第一个”结点,当Thread-1被唤醒并获得锁成功时,其前面的Dummy结点出队列,而Thread-1这个结点还在阻塞队列中,因为他有义务去唤醒他后面的结点,也就是说,阻塞队列中的第一个结点如果不是Dummy,代表第一个结点此时正持有锁,所以自然阻塞队列的第二个醒来后可以去竞争锁了。 -
接下来是释放锁的过程
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
首先调用tryRelease()尝试释放锁,该方法由子类特定实现,在ReentrantLock中实际上是判断是否是重入锁,如果是就让state减一,如果不是就释放锁,释放锁成功后由该线程唤醒其在阻塞队列中的后一个结点,并进行unpark,之后被唤醒的结点又回到前面的操作,周而复始。
综上,我个人把AQS总结为,一个实现了完整的阻塞与唤醒策略的成熟的锁框架。