AQS源码分析

概述

AQS是java concurrent包的基础,像Lock、CountDownLatch、Semaphore等都是基于它实现的;

成员变量

private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
static final long spinForTimeoutThreshold = 1000L;
  • head:等待队列头部,延迟初始化,直到调用enq才真正初始化;
  • tail:等待队列尾部,延迟初始化,直到调用enq才真正初始化;
  • state:AQS状态位,通过try*方法维护;
  • spinForTimeoutThreshold:自旋锁超时阀值;

实际上head是个空节点,其thread和prev属性都为null;

Node内部类

AQS会将等待线程封装成Node,下面看看Node类的结构:

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;//表示下一个节点是通过park堵塞的,需要通过unpark唤醒
    static final int CONDITION = -2;//表示线程在等待条件变量(先获取锁,加入到条件等待队列,然后释放锁,等待条件变量满足条件;只有重新获取锁之后才能返回)
    static final int PROPAGATE = -3;//表示后续结点会传播唤醒的操作,共享模式下起作用

    //等待状态:对于condition节点,初始化为CONDITION;其它情况,默认为0,通过CAS操作原子更新
    volatile int waitStatus;
    //前节点
    volatile Node prev;
    //后节点
    volatile Node next;
    //线程对象
    volatile Thread thread;
    //对于Condtion表示下一个等待条件变量的节点;其它情况下用于区分共享模式和独占模式;
    Node nextWaiter;

    final boolean isShared() {
        return nextWaiter == SHARED;//判断是否共享模式
    }
    //获取前节点,如果为null,抛出异常
    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(Thread thread, Node mode) {     //addWaiter方法使用
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { //Condition使用
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

可以看到Node中记录了等待的线程对象、节点状态和前后节点,并且通过nextWaiter判断是独占还是共享模式:

  • 独占模式:每次只能有一个线程能持有资源;
  • 共享模式:允许多个线程同时持有资源;

例如:

  1. CountDownLatch的await方法可以在多个线程中调用,当CountDownLatch的计数器为0后,调用await的方法都会依次返回。 也就是说多个线程可以同时等待await方法返回,因此它适合被设计成共享模式,因为它获取的是一个共享资源,资源在所有调用await方法的线程间共享;
  2. ReentrantLock提供了lock和unlock方法,只允许一个线程获得锁,因此它适合被设计成独占模式,因为它获取的是一个独占资源,资源不能在调用lock方法的线程间共享;
  3. Semaphore维护了一组许可,acquire方法获取许可,如果有可用的许可,方法返回,否则block;可用看到,acquire获取到也是一个共享资源,只不过资源的数量有限制,因此它适合被设计成共享模式;
  4. ReentrantReadWriteLock提供了读写锁,写操作是独占的,读操作是可以彼此共享的,因此它同时使用了独占和共享模式;

抽象方法说明

AbstractQueuedSynchronizer是个抽象类,部分方法并未实现,子类可以根据实际情况实现全部或部分方法:

    //非堵塞获取独占资源,true表示成功
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    //非堵塞释放独占资源,true表示成功
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    //非堵塞获取共享资源,负数表示失败,0表示成功但不需要向后传播,大于0表示成功且可以向后传播
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //非堵塞释放共享资源,true表示成功
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //在排它模式下,状态是否被占用
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

可以看到这些方法主要包括两组:独占方法和共享方法;一般而言,子类只需要实现其中一个模式即可,因此AQS并没有将这些方法定义为抽象的;

独占模式

acquire

独占模式获取资源;

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • 调用tryAcquire,如果返回false,表示获取资源失败,要进行排队获取;
  • 调用addWaiter,创建独占模式Node,并加入到等待队列的尾部;
  • 调用acquireQueued方法,按照线程加入队列的顺序获取资源;
  • 如果acquireQueued返回true,表示发生中断,因此通过selfInterrupt中断当前线程;
    注意:acquire方法会忽略中断,当中断发生时,并不会马上退出;

上面的第2步调用了addWaiter方法,方法实现如下:

private Node addWaiter(Node mode) {
   //根据传入的模式(独占or共享)创建Node对象;
    Node node = new Node(Thread.currentThread(), mode);
    
    Node pred = tail;
   //如果pred不为空,说明有线程在等待
   //尝试使用CAS入列,如果入列失败,则调用enq采用自旋的方式入列
   //该逻辑在无竞争的情况下才会成功,快速入列
    if (pred != null) {
       //所谓的入列,就是将节点设置为新的tail节点
       //注意:有可能设置node的前节点成功,但是CAS更新失败;
       //这种情况下,由于无法从head或tail找到节点,问题不大;
       //但是对于isOnSyncQueue这种方法,则会造成影响,需要特殊处理
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {//通过CAS更新tail节点,关于CAS,后面会专门写篇文章介绍
           //将原tail节点的后节点设置为新tail节点
           //由于CAS和设置next不是原子操作,因此可能出现更新tail节点成功,但是未执行pred.next = node,导致无法从head遍历节点;
           //但是由于前面已经设置了prev属性,因此可以从尾部遍历;
           //像getSharedQueuedThreads、getExclusiveQueuedThreads都是从尾部开始遍历
            pred.next = node;
            return node;
        }
    }
    enq(node);//通过自旋入列
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;//记录尾节点
        if (t == null) { //由于采用lazy initialize,当队列为空时,需要进行初始化
            //通过CAS设置head和tail节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;//将node的前节点设置为原tail节点
            if (compareAndSetTail(t, node)) {//CAS更新tail节点,更新成功则将原tail节点的后节点设置为node,返回原tail节点,入列成功;
                t.next = node;
                return t;
            }
        }
    }
}

可以看到即使存在多线程竞争,例如线程1通过compareAndSetHead初始化了head和tail节点,线程2此时运行到**if (t == null) **,发现判断成立,通过CAS更新head节点,此时会更新失败,继续下一循环;直到线程1执行完tail=head,线程2才会进入else逻辑,节点入列;可以看到:

  • head节点实际上是个空节点;
  • head节点是通过new Node()创建,因此waitStatus==0;
  • 新入列的节点是通过Node(Thread thread, Node mode)创建,waitStatus==0;

下面继续看acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;//未发生中断
        //仍然通过自旋,根据前面的逻辑,此处传入的为新入列的节点
        for (;;) {
            final Node p = node.predecessor();//获取前节点,即prev指向节点
           //如果node的前一节点为head节点,而head节点为空节点,说明node是等待队列里排在最前面的节点
            if (p == head && tryAcquire(arg)) {
              //获取资源成功,将node设置为头节点,setHead清空节点属性thread,prev
                setHead(node);
                p.next = null; // 将原头节点的next设为null,帮助GC
                failed = false;
                return interrupted;//返回是否发生中断
            }
            //如果acquire失败,是否要park,如果是则调用LockSupport.park
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;//发生中断
        }
    } finally {
        if (failed)//只有循环中出现异常,才会进入该逻辑
            cancelAcquire(node);
    }
}

acquireQueued调用了shouldParkAfterFailedAcquireparkAndCheckInterrupt方法,这两个方法分别是干什么的呢?

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //如果acquireQueued第一次调用该方法,ws==0
    int ws = pred.waitStatus;
   //已经设置了状态,由于SIGNAL表示要通过unpark唤醒后一节点,因此当获取失败时,是要调用park堵塞的,返回true
    if (ws == Node.SIGNAL)
        return true;
    //如果前一节点已取消,则往前找,直到找到一个状态正常的节点,其实就是从队列删除取消状态的节点
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;//更新next指针,去掉中间取消状态的节点
    } else {//更新pred节点的waitStatus为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;//返回false,表示不需要调用park
}
private final boolean parkAndCheckInterrupt() {
   //将当前线程的parkBlocker变量指向this,调用unsafe.park堵塞当前线程
   //简单来说park是申请许可,如果存在许可,马上返回,否则一直等待获得许可;unpark是将许可数量设为1,会唤醒park返回;
 //LockSupport提供了unpark(Thread thread)方法,可以为指定线程颁发许可
   //如果想更多了解,请阅读《Java如何实现线程堵塞》这篇文章
    LockSupport.park(this);
    return Thread.interrupted();//注意:该方法会清除线程的中断状态
}

可以看到对于等待队列中的节点,shouldParkAfterFailedAcquire会将前节点的状态改为Node.SIGNAL;接着在下一次循环中调用parkAndCheckInterrupt堵塞线程

最后看看cancelAcquire方法:

private void cancelAcquire(Node node) {
    if (node == null)
        return;

    node.thread = null;
   //获取node的前向节点
    Node pred = node.prev;
   //如果发现前向节点状态为CANCELLED,则继续向前找,直到找到状态正常的节点
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    
    Node predNext = pred.next;
    node.waitStatus = Node.CANCELLED;//节点状态设为CANCELLED
    //如果node为tail节点,则将pred更新为tail节点
    if (node == tail && compareAndSetTail(node, pred)) {
       //由于pred为新的尾节点,因此将其next设为null
        compareAndSetNext(pred, predNext, null);
    } else {//如果node不是尾节点
        int ws;
        //当满足下面三个条件,将pred的next指向node的下一节点:
        //1.pred不是head节点:如果pred为头节点,而node又被cancel,则node.next为等待队列中的第一个节点,需要unpark唤醒
        //2.pred节点状态为SIGNAL或能更新为SIGNAL
        //3.pred的thread变量不能为null
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            //更新pred的next,指向node的下一节点
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);//如果pred为头节点,则唤醒node的后节点
        }
        node.next = node; // help GC
    }
}

unparkSuccessor方法唤醒下一节点:

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
   
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
   //如果节点为空或者被取消了,则从队列尾部开始查找,找到离node最近的非null且状态正常的节点
    Node s = node.next;
    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;
    }
    //取出找到节点的线程对象,通过unpark,颁发许可;
    if (s != null)
        LockSupport.unpark(s.thread);
}

acquireInterruptibly

该方法和acquire类似,只不过发生中断时,会抛出InterruptedException;

release

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//unpark唤醒第一个等待节点
        return true;
    }
    return false;
}

可以看到逻辑很简单,从等待队列中取出第一个等待的节点,通过unparkSuccessor调用unpark释放资源;

共享模式方法

acquireShared

acquireShared用于共享模式下获取资源,该方法会忽略中断:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

tryAcquireShared方法AQS未实现,留待子类实现;主要看看doAcquireShared的逻辑:

private void doAcquireShared(int arg) {
   //创建共享节点并加入到等待队列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {//如果是第一个等待节点,则调用tryAcquireShared
               //负数表示失败,0表示成功当无法传播,1表示成功且可传播
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                   //设置head节点,检查下一个等待节点是否是共享模式,如果是,向下传播
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();//恢复中断状态
                    failed = false;
                    return;
                }
            }
           //前面已经有介绍
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

可以看到acquire调用的是setHead,而acquireShared调用的是** setHeadAndPropagate**:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);//前见面的分析
   //1.传入propagate>0
   //2.head为null:什么情况会变为null?
   //3. 之前操作已经设置了后续节点需要唤醒
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {//如果等待队列中有等待线程
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {//需要unpark后续节点
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

从上面的分析可以知道,独占模式和共享模式的最大区别在于独占模式只允许一个线程持有资源,而共享模式下,当调用doAcquireShared时,会看后续的节点是否是共享模式,如果是,会通过unpark唤醒后续节点;
从前面的分析可以知道,被唤醒的节点是被堵塞在doAcquireShared的parkAndCheckInterrupt方法,因此唤醒之后,会再次调用setHeadAndPropagate,从而将等待共享锁的线程都唤醒,也就是说会将唤醒传播下去;

  1. 加入同步队列并阻塞的节点,它的前驱节点只会是SIGNAL,表示前驱节点释放锁时,后继节点会被唤醒。shouldParkAfterFailedAcquire()方法保证了这点,如果前驱节点不是SIGNAL,它会把它修改成SIGNAL。
  2. 造成前驱节点是PROPAGATE的情况是前驱节点获得锁时,会唤醒一次后继节点,但这时候后继节点还没有加入到同步队列,所以暂时把节点状态设置为PROPAGATE,当后继节点加入同步队列后,会把PROPAGATE设置为SIGNAL,这样前驱节点释放锁时会再次doReleaseShared,这时候它的状态已经是SIGNAL了,就可以唤醒后续节点了。
    举例说明:例如读写锁,写读操作和写写操作互斥,读读之间不互斥;当调用acquireShared获取读锁时,会检查后续节点是否是获取读锁,如果是,则同样释放;

releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

doReleaseShared会唤醒第一个等待节点, 根据前面acquireShared的逻辑,被唤醒的线程会通过setHeadAndPropagate继续唤醒后续等待的线程。

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

推荐阅读更多精彩内容