有没有想过ReentrantLock,Semaphores等是怎么实现同步的?这一切归功于幕后功臣AQS,全名AbstractQueuedSynchronizer--抽象的队列同步器
jdk文档上的说明:
提供一个依赖于先进先出(FIFO)等待队列,实现阻塞锁和相关同步器(信号量、事件等)的框架
也就是说,AbstractQueuedSynchronizer是一个用队列来实现的同步框架
首先瞄一瞄它的子类
再勾画出它的继承图
可以看到,ReentrantLock,Semaphores,ThreadPoolExecutor,CountDownLatch,ReentrantReadWriteLock的同步机制的实现,都依赖于AbstractQueuedSynchronizer
那么它们是怎么通过AQS实现同步的呢?
//ReentrantLock类的公平锁实现
final void lock() {
acquire(1);
}
//ReentrantLock类的非公平锁实现
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//ReentrantReadWriteLock类lock方法实现
public void lock() {
sync.acquire(1);
}
可以看到这几种锁的实现都依赖AQS类的一个很重要的方法--acquire,我们一起来揭开acquire神秘的内衣,不,面纱
/**
* 独占模式获取
* AQS类的子类需实现本方法里的tryAcquire方法
* 否则,线程将排队,可能会重复阻塞和解除阻塞
* 直到调用tryAcquire成功为止.
* 该方法可用于实现锁方法
*
* @param arg
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
此方法是使当前线程获取独占模式,说人话就是获取线程的运行权,使线程进入RUNNABLE状态.
在acquire里,首先调用tryAcquire方法,如果tryAcquire返回true,线程直接变为运行状态,若不成功则调用acquireQueued和addWaiter方法,使线程进入CLH队列,这个后面会说到.
但我们看到tryAcquire直接抛异常了,什么鬼!我来翻译一下jdk文档的注释
/**
* 试图以独占模式获取.
* 此方法应该查询状态是否允许存在独占模式,允许才进行获取
*
* @param arg 此参数一般传1,不过如你有其他需要传多少都行
* @return {@code true}
* @throws IllegalMonitorStateException 如果AQS的状态是非法状态就会抛出次异常
* @throws UnsupportedOperationException 如果实现类不支持独占模式就会抛出此异常
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
原来,AQS类的子类需覆盖tryAcquire方法,直接抛异常是考虑到,子类若不支持独占锁模式,就无须覆盖此方法,调用的时候自然就抛出UnsupportedOperationException.还有arg参数一般传1,这是因为可重入锁每次重入,状态state+1.
至于各种Lock是怎么覆盖tryAcquire方法,以及重入状态为什么是+1,请参考线程八锁
我们继续来扒一扒acquireQueued方法,在说这个方法前,先说说AQS的底层数据结构--CLH队列--的变体--不知道什么鬼队列.反正它叫队列同步器,顾名思义,需要一个队列来控制同步,而队列的节点依赖于内部类Node
/**
* 等待队列节点类
* 此等待队列是CLH锁队列的一种变体
* 每个节点都有一个"status"字段来表示该节点中的线程是否应该被阻塞
*/
static final class Node {
/**
* 共享节点
*/
static final Node SHARED = new Node();
/**
* 独占节点
*/
static final Node EXCLUSIVE = null;
/**
* 表示线程已被取消
*/
static final int CANCELLED = 1;
/**
* 表示当前节点的后继线程需要阻塞
*/
static final int SIGNAL = -1;
/**
* 表示线程在等待条件
*/
static final int CONDITION = -2;
/**
* 表示下一次acquireShared应该无条件传播
*/
static final int PROPAGATE = -3;
/**
* SIGNAL: 此节点的后继线程是阻塞的或即将阻塞,所以此节点的线程释放或被取消时,必须唤醒它的后
* 继.为了避免产生竞争,获取方法必须表明它们需要一个信号,然后重新原子获取,若失败则阻塞
* CANCELLED: 由于超时或中断,此节点被取消.节点永远不会离开此状态.特别是,
* 节点的线程处于取消状态再也不会阻塞
* CONDITION: 此节点当前位于条件队列中.它不会用作同步队列节点,直至状态改变为止,当时的状态
* 将被设置为0
* PROPAGATE: 已发布的节点应该传播到其他节点。这在doReleaseShared中设置(仅针对head节
* 点),以确保传播继续进行,即使其他操作已经介入。
* 0: 以上都不是
*/
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
//将线程构造成一个Node,添加到等待队列
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//这个方法会在Condition队列使用
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
从源码可以看出,一个Node对象里面包含前继节点,后继节点和一个线程对象,所以Node对象组成的队列是基于双向链表的FIFO(先进先出)队列.大概长这样(懒的画,偷了张图回来,既然上面有水印我就不注明出处了)
先看看addWaiter源码
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
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;//将尾节点作为新节点的前继
if (compareAndSetTail(pred, node)) {//CAS把新节点设置为尾节点
pred.next = node;//把原尾节点的后继设置为新节点
return node;
}
}
enq(node);
return node;
}
/**
* 通过自旋操作把当前节点加入到队列尾部
*/
private Node enq(final Node node) {
for (; ; ) {
Node t = tail; //如果是第一次添加到队列,那么tail=null
if (t == null) { // Must initialize
//CAS的方式创建一个空的Node作为头结点
if (compareAndSetHead(new Node()))
//此时队列中只一个头结点,所以tail也指向它
tail = head;
} else {
//进行第二次循环时,tail不为null,进入else区域。
//将当前线程的Node结点的prev指向tail,然后使用CAS将tail指向Node
node.prev = t;
if (compareAndSetTail(t, node)) {
//t此时指向tail,所以可以CAS成功,将tail重新指向Node。
//此时t为更新前的tail的值,即指向空的头结点,t.next=node,就将头结点的后续结点指向Node,返回头结点
t.next = node;
return t;
}
}
}
}
addWaiter方法大致流程是:
- 创建当前线程节点
- 若尾节点不为空,则把新节点添加到尾部,成为新的尾节点
- 若尾节点为空,则调用enq方法,首先初始化队列,第二次循环时,再把新节点添加到尾部,成为新的尾节点
再看看acquireQueued的源码
/**
* 获取已处于独占不可中断模式的线程队列
* @return 是否在等待时被打断
*/
final boolean acquireQueued(/**此node为addWaiter所返回,即已成为尾节点的当前线程节点**/final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {//自旋
final Node p = node.predecessor();//获取前继节点
//若前继是head节点才有资格获取锁
if (p == head &&
//在这里还是会调用tryAcquire
tryAcquire(arg)) {
//若获取锁成功,设置当前节点为头节点
setHead(node);
//凡是head节点,head.thread与head.prev永远为null, 但是head.next不为null
p.next = null; // help GC--有助于GC回收
failed = false;
return interrupted;
}
//如果获取锁失败或前继节点不是头节点,则根据节点的waitStatus决定是否需要挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())// 若前面为true,则执行挂起,待下次唤醒的时候检测中断的标志
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);// 如果抛出异常则取消锁的获取,进行出队(sync queue)操作
}
}
小结acquireQueued的大致流程:
- 判断当前节点的前驱节点是否头节点
- 若是,则尝试获取锁,如果获取锁成功,则设置当前为头节点
- 若当前节点的前驱节点不是头节点或尝试获取锁失败,则根据节点的waitStatus决定是否需要挂起线程
- 若上述过程出现异常(因为上述操作在自旋中,不出现异常不会跳到finally代码块中),则判断头节点是否设置失败,若失败,则取消正在进行的获取锁的尝试
释放锁移除节点
head节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下
- 修改head节点指向下一个获得锁的节点
- 新的获得锁的节点,将prev的指针指向null
这里有一个小的变化,就是设置head节点不需要用CAS,原因是设置head节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要CAS保证,只需要把head节点设置为原首节点的后继节点,并且断开原head节点的next引用即可
对应源码
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(long arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
参考文献
深入分析AQS实现原理