浅析AQS

有没有想过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方法大致流程是:

  1. 创建当前线程节点
  2. 若尾节点不为空,则把新节点添加到尾部,成为新的尾节点
  3. 若尾节点为空,则调用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的大致流程:

  1. 判断当前节点的前驱节点是否头节点
  2. 若是,则尝试获取锁,如果获取锁成功,则设置当前为头节点
  3. 若当前节点的前驱节点不是头节点或尝试获取锁失败,则根据节点的waitStatus决定是否需要挂起线程
  4. 若上述过程出现异常(因为上述操作在自旋中,不出现异常不会跳到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实现原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容