AQS详解

欢迎大家关注公众号——秃头让我们变强

AQS简介

编程不识Doug Lea,写尽Java也枉然。JUC包的作者,大名鼎鼎的Doug Lea,在JUC包中为我们留下了一个AbstranctQueuedSynchronizer类。希望它能满足一切并发编程开发者的同步需求。JUC包中的锁也基本都是基于AQS来实现的。AQS提供了一个volatile类型变量作为获取资源成功的标志,内部维护着一个FIFO同步队列,将获取锁失败的线程构造成一个Node节点加入同步队列中去,Node为AQS的一个内部类。并且提供了共享和独享模式的获取锁。

 

独享模式获取

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire(arg)方法在AQS中并没有实现,需要开发者自己去实现,成功返回true,失败返回false。然后调用addWaiter()方法将当前线程构造成一个独享模式的节点。

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
     
        //获取当前同步队列的尾节点
        Node pred = tail;
        if (pred != null) {
            //将当前节点的前驱指向尾节点
            node.prev = pred;
             //CAS设置当前节点为尾节点
            if (compareAndSetTail(pred, node)) {
                //将旧的尾节点的后继指针指向当前节点
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
}

可以看到,如果当前队列的尾节点不为Null,则CAS快速设置尾节点。若为Null则调用enq(node)方法设置尾节点

private Node enq(final Node node) {
    for (;;) {
        // 获取当前尾节点
        Node t = tail;
        if (t == null) { 
            // 说明队列是空的,初始化一下。设置一个空的头节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 把当前节点的前驱指向尾节点
            node.prev = t;
            //CAS设置当前节点为尾节点
            if (compareAndSetTail(t, node)) {
                //将旧的尾节点的后继指针指向当前节点
                t.next = node;
                return t;
            }
        }
    }
}

接着,当addWaiter()成功返回当前节点后,调用acquireQueued(Node node)方法,将当前节点加入同步队列。

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)) {  
                    //把当前节点设置为头节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //shouldParkAfterFailedAcquire判断当前节点是否需要阻塞
                //parkAndCheckInterrupt阻塞当前节点
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

分析一下shouldParkAfterFailedAcquire()方法,在此之前,我们先看一下AQS内部类,即节点类

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;  //表示当前节点在等待队列中       
    static final int PROPAGATE = -3;  //节点的共享状态将被传播下去         
    volatile int waitStatus;        
    //前驱节点        
    volatile Node prev;        
    //后继节点        
    volatile Node next;        
    volatile Thread thread;        
    Node nextWaiter;        
    //省略其他代码
}

知道了节点的状态,我们来看一下shouldParkAfterFailedAcquire方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {       
    //获取前驱节点的状态        
    int ws = pred.waitStatus;          
    //如果节点状态为SIGNAL,即正常          
    if (ws == Node.SIGNAL)               
    //表示当前线程需要阻塞                  
        return true;            
    //即CANCELLED,表示前驱节点的线程中断了,节点需要从同步队列取消               
    if (ws > 0) {                
        do {                    
            //从后往前找,直到找到第一个节点状态非CANCELLED的                    
            node.prev = pred = pred.prev;            
        } while (pred.waitStatus > 0);            
        //将找到的节点的后继指针指向当前节点           
        pred.next = node;        
    } else {          
        //将前驱节点的状态设置为SIGNAL           
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    
    }       
    return false;
}

如果shouldParkAfterFailedAcquire方法返回true,则代表当前线程需要阻塞,若返回false,也会在下一次循环中返回true。下面看一下parkAndCheckInterrupt()方法是如何让线程阻塞的。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
}

可以看到,很简单,就是调用LockSuppor.park方法阻塞。当线程被唤醒时,会返回当前线程的中断标志位状态。

 

独享式获取总结

现在我们来总结一下流程:

  1. 调用tryacquire()方法尝试获取锁,若获取失败则执行步骤2

  2. 调用addWaiter()方法,将当前线程构造成一个节点,如果当前队列不为空,则把当前节点设置为尾节点,若为空,则先初始队列,然后将当前节点设置为尾节点,继续执行步骤3

  3. 调用acquireQueued()方法,判断当前节点的前驱节点是否是头节点,如果是则尝试获取锁,若获取成功,则把当前节点设为头节点并返回成功。如果前驱节点不是头节点或获取锁失败,则执行步骤4

  4. 调用shouldParkAfterFailedAcquire()方法,判断当前线程是否需要阻塞,如果需要,则调用parkAndCheckInterrupt()方法阻塞当前线程。当线程被唤醒时需要检查线程的中断标志位。

  5. 以上步骤3和步骤4是自旋的。

独享式释放锁

这里tryRelease(arg)方法同样是需要子类去实现,失败返回false,成功返回true。

public final boolean release(int arg) {
        //尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            //释放成功,唤醒后继节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

看一下unparkSuccessor(node)方法,是如何唤醒后继节点的

private void unparkSuccessor(Node node) {
        //获取当前节点的状态
        int ws = node.waitStatus;
        //如果非CANCELLED
        if (ws < 0)
            //设置状态为初始状态
            compareAndSetWaitStatus(node, ws, 0);
        //获取后继节点
        Node s = node.next;
        //如果后继节点为null或是CANCELLED状态
        if (s == null || s.waitStatus > 0) {
            //节点需要取消,helpGC
            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);
}

独享式释放总结

其实独享式释放还是很简单的。

  1. 调用tryRelease(arg)方法尝试释放锁,释放失败返回false,释放成功进入步骤2

  2. 获取头节点,如果头节点不为Null并且不是初始的节点,则执行步骤3。否则直接返回true,释放成功。

  3. 调用unparkSuccessor(node)方法,先检查当前节点的状态,如果非CANCELLED状态,则CAS设置当前节点状态为0(即初始状态)。获取当前节点的后继节点,若后继节点是CANCELLED状态,把后继节点的引用置Null,帮助GC回收。接着从后往前遍历队列,找到第一个状态非CANCELLED的节点。如果找到了则执行步骤4

  4. 调用LockSupport.unpark()唤醒节点

 

共享式获取

先看一下acquireShared()方法

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

同样的,tryAcquireShared()方法没有具体的实现,需要子类自己去实现。

protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

接着我们看看当获取同步状态失败后,执行的doAcquireShared()方法。

private void doAcquireShared(int arg) {
    //addWaiter()方法同之前独享模式一样,将当前线程构造成一个节点
    // 加入到同步队列的尾节点,不同但是,节点的模式是SHARED,共享
    final Node node = addWaiter(Node.SHARED);
    //获取同步状态成功标志
    boolean failed = true;
    try {
        // 线程中断标志
        boolean interrupted = false;
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头节点
            if (p == head) {
                // 尝试获取同步状态
                int r = tryAcquireShared(arg);
                // 获取成功
                if (r >= 0) {
                    // 设置当前节点为头节点,稍后详细分析此方法
                    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);
    }
}

从前面的分析,我们可以看到,共享模式的同步状态获取和独享差不多,主要区别就在获取成功后设置头节点不同。那么我们来分析一下setHeadAndPropagate()方法。

private void setHeadAndPropagate(Node node, int propagate) {
    // 获取当前头节点,即旧的头节点
    Node h = head; // Record old head for check below
    // 设置当前节点为头节点
    setHead(node);
    // 注释1:这个if条件判断很有趣,后面分析
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        // 获取当前节点的后继节点
        Node s = node.next;
        // 后继节点为null或是共享模式
        if (s == null || s.isShared())
            // 唤醒节点
            doReleaseShared();
    }
}

看到这里你或许会疑问,前面我注释1的那个if条件判断到底该如何理解呢?

  1. “propagate > 0”,代表还有同步资源可以获取,那当然要唤醒后继节点咯。

  2. h==null,共享模式下,同步资源的获取和释放都是并发的,所以在当前节点获取同步资源成功,执行到if判断前,旧的头节点变为Null,那当然是释放了资源,所以要继续唤醒后面的节点

  3. (h=head)==null,这里可能会有疑问,当前节点刚获取到同步资源都还未执行完方法怎么会为null呢,其实这里h=head,head不一定是当前节点。为什么呢?之前说了这个方法是并发的,那么在当前节点执行完setHead设置成head后,还未执行到if判断,此时又有新的节点获取到同步资源,然后setHead,此时head就不是当前节点了,那么这就跟问题2一样了。

  4. h.waitStatus < 0 和 head.waitStatus < 0,其实是为了提高吞吐率,当一个节点成功获取到同步资源时,后面的节点也极有可能获取到资源。所以为了保守起见,waitStatus < 0时也尝试唤醒后继节点。这一点在源码注释中,Doug lea大神也说了。

 

我们继续分析doReleaseShared方法

private void doReleaseShared() {
    for (;;) {
        // 获取头节点,注意这里的头节点就是当前获取同步状态成功的节点
        Node h = head;
        // 头节点不是null且不等于尾节点,即当前获取同步状态成功
        // 的节点还有后继节点需要唤醒
        if (h != null && h != tail) {
            // 获取头节点的状态
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // CAS设置头节点的状态为0,直到成功为止。因为这个方法
                // 在获取和释放都调用到了,是并发的。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒后继节点,稍后分析此方法          
                unparkSuccessor(h);
            }
            // 如果后继节点不需要唤醒,则把当前节点设置为PROPAGATE,确保以后可以传播下去
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;          
        }
        //如果头节点没变化,说明没有其他线程干预,如果有,则需要重新设置
        if (h == head)              
            break;
    }
}

接着分析unparkSuccessor方法是如何唤醒节点的

private void unparkSuccessor(Node node) {
    // 当前节点状态
    int ws = node.waitStatus;
    // 非CANCELLED
    if (ws < 0)
        // CAS修改当前节点状态为0
        compareAndSetWaitStatus(node, ws, 0);
    // 当前节点的后继节点
    Node s = node.next;
    // 后继节点为Null或CANCELLED
    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);
}

分析完共享模式的获取方法,再来看释放方法就会很简单

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        // 此方法在上面分析获取时,已经分析
        doReleaseShared();
        return true;
    }
    return false;
}

同样的,tryReleaseShared()方法需要我们自己去实现,成功返回true,失败返回false。而doReleaseShared()方法,跟前面获取调用的是同一个方法,所以不再叙述。这也应征了前面所说的,共享模式的获取和释放是并发的。

 

结语

至此,我们通过两篇文章学习了AQS共享模式和独占模式下的同步资源获取/释放。对AQS也有了较深的理解。可以看到AQS是相当重要的,也深深体会到Doug lea大神的超人智慧。希望有朝一日,我们在座所有的各位都能有这样精妙的设计。Thanks。

 

更多请关注公众号——秃头让我们变强

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值