AbstractQueuedSynchronizer(二)

本文深入解析了AQS(AbstractQueuedSynchronizer)的工作原理及其实现机制,包括其内部的数据结构、关键方法如acquire与release的流程,以及如何通过自定义同步器实现对共享资源的管理和控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载自:http://www.cnblogs.com/leesf456/p/5350186.html
              http://www.cnblogs.com/waterystone/p/4920797.html

       AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架,如ReentrantLock、ReentrantReadWriteLock、Semaphore等,它是JUC并发包中的核心基础组件。
       AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。AQS使用一个int类型的成员变量state来代表共享资源和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对state进行操作,当然AQS可以确保对state的操作是安全的。

       AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。AbstractQueuedSynchronizer类的数据结构如下:
      
       AbstractQueuedSynchronizer类底层的数据结构是使用双向链表,是队列的一种实现,故也可看成是队列,其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。

       AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():  程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):  独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):  独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):  共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):  共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
       以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
       再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。


类定义

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private static final long serialVersionUID = 7373984972572414691L;

    protected AbstractQueuedSynchronizer() { }

    // 头结点
    private transient volatile Node head;    
    // 尾结点
    private transient volatile Node tail;    
    // 状态
    private volatile int state;    
    // 自旋时间
    static final long spinForTimeoutThreshold = 1000L;

内部类Node

static final class Node {
        // 模式,分为共享与独占
        // 共享模式
        static final Node SHARED = new Node();
        // 独占模式
        static final Node EXCLUSIVE = null;        
        // 结点状态
        // CANCELLED,值为1,表示当前的线程被取消
        // SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
        // CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
        // PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
        // 值为0,表示当前节点在sync队列中,等待着获取锁
        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;
        
        // 结点是否在共享模式下等待
        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(Thread thread, Node mode) {    // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        
        // 构造函数
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
       每个被阻塞的线程都会被封装成一个Node结点,放入队列。每个节点包含了一个Thread类型的引用,并且每个节点都存在一个状态,具体状态如下。CANCELLED表示当前的线程被取消;SIGNAL表示当前节点的后继节点包含的线程需要运行,需要进行unpark操作;CONDITION表示当前节点在等待condition,也就是在condition queue中;PROPAGATE表示当前场景下后续的acquireShared能够得以执行;值0表示当前节点在sync queue中,等待着获取锁。

acquire函数

       此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
       首先调用tryAcquire函数,调用此方法的线程会试图在独占模式下获取对象状态。此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它并则直接返回。在AbstractQueuedSynchronizer源码中默认会抛出一个异常,即需要子类去重写此函数完成自己的逻辑。
       若tryAcquire失败,则调用addWaiter函数,addWaiter函数完成的功能是将调用此方法的线程封装成为一个结点并放入Sync queue。
       调用acquireQueued函数,此函数完成的功能是Sync queue中的结点不断尝试获取资源,若成功,则返回true,否则,返回false。

       首先分析addWaiter(Node.EXCLUSIVE):
    // 添加等待者
    private Node addWaiter(Node mode) {
        // 新生成一个结点,默认为独占模式
        Node node = new Node(Thread.currentThread(), mode);
        // 保存尾结点
        Node pred = tail;
        if (pred != null) { // 尾结点不为空,即已经被初始化
            // 将node结点的prev域连接到尾结点
            node.prev = pred; 
            if (compareAndSetTail(pred, node)) { // 比较pred是否为尾结点,是则将尾结点设置为node 
                // 设置尾结点的next域为node
                pred.next = node;
                return node; // 返回新生成的结点
            }
        }
        // 上一步失败则通过enq入队
        enq(node); // 尾结点为空(即还没有被初始化过),或者是compareAndSetTail操作失败,则入队列
        return node;
    }
       通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。那么该线程下一步即进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源。即acquireQueued 函数的作用:
    // sync队列中的结点在独占且忽略中断的模式下获取(资源)
    final boolean acquireQueued(final Node node, int arg) {
        // 标志
        boolean failed = true;
        try {
            // 中断标志
            boolean interrupted = false;
            for (;;) { // 自旋
                // 获取node节点的前驱结点
                final Node p = node.predecessor(); 
                if (p == head && tryAcquire(arg)) { // 前驱为头结点,说明当前节点是队列第一个等待的节点,并且成功获得锁
                    setHead(node); // 设置自己为头结点
                    p.next = null; //方便GC回收以前的head结点。
                    failed = false; // 设置标志
                    return interrupted; 
                }


                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
       首先获取当前节点的前驱节点,如果前驱节点是头结点并且能够获取资源,代表该当前节点能够占有锁,设置头结点为当前节点,返回。否则,调用shouldParkAfterFailedAcquire和parkAndCheckInterrupt函数,shouldParkAfterFailedAcquire函数如下:
    // 当获取(资源)失败后,检查并且更新结点状态
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取前驱结点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) // 状态为SIGNAL,为-1
            // 可以进行park操作
            return true; 
        if (ws > 0) { // 表示状态为CANCELLED,为1
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0); // 找到pred结点前面最近的一个状态不为CANCELLED的结点,即使pred结点为正常等待状态的结点
            // 赋值pred结点的next域
            pred.next = node; 
        } else { // 为PROPAGATE -3 或者是0 表示无状态,(为CONDITION -2时,表示此节点在condition queue中) 
            // 如果前驱正常,那就把前驱的状态设置成SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 
        }
        // 不能进行park操作
        return false;
    }
       只有当该节点的前驱结点的状态为SIGNAL时,才可以对该结点所封装的线程进行park操作。否则,将不能进行park操作。再看parkAndCheckInterrupt函数,源码如下:
    // 进行park操作并且返回该线程是否被中断
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); //调用park()使线程进入waiting状态
        return Thread.interrupted(); // 当前线程是否已被中断,并清除中断标记位
    }
       parkAndCheckInterrupt函数里的逻辑是首先执行park操作,然后返回该线程是否已经被中断。park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:被unpark()或被interrupt()。

       再看final块中的cancelAcquire函数,其源码如下:
    // 取消继续获取(资源)
    private void cancelAcquire(Node node) {
        if (node == null)
            return;
        // 设置node结点的thread为空
        node.thread = null;

        // 保存node的前驱结点
        Node pred = node.prev;
        while (pred.waitStatus > 0) // 找到node前驱结点中第一个状态小于0的结点,即不为CANCELLED状态的结点
            node.prev = pred = pred.prev;

        // 获取pred结点的下一个结点
        Node predNext = pred.next;

        // 设置node结点的状态为CANCELLED
        node.waitStatus = Node.CANCELLED;

        if (node == tail && compareAndSetTail(node, pred)) { // node结点为尾结点,则设置尾结点为pred结点
            // 比较并设置pred结点的next节点为null
            compareAndSetNext(pred, predNext, null); 
        } else { // node结点不为尾结点,或者比较设置不成功
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) { // (pred结点不为头结点,并且pred结点的状态为SIGNAL)或者 
                                    // pred结点状态小于等于0,并且比较并设置等待状态为SIGNAL成功,并且pred结点所封装的线程不为空
                // 保存结点的后继
                Node next = node.next;
                if (next != null && next.waitStatus <= 0) // 后继不为空并且后继的状态小于等于0
                    compareAndSetNext(pred, predNext, next); // 比较并设置pred.next = next;
            } else {
                unparkSuccessor(node); // 释放node的前一个结点
            }

            node.next = node; // help GC
        }
    }
       该函数完成的功能就是取消当前线程对资源的获取,即设置该结点的状态为CANCELLED,接着再看unparkSuccessor函数,此方法用于唤醒等待队列中下一个线程,源码如下:
    // 释放后继结点
    private void unparkSuccessor(Node node) {
        // 获取node结点的等待状态
        int ws = node.waitStatus;
        if (ws < 0) // 状态值小于0,为SIGNAL -1 或 CONDITION -2 或 PROPAGATE -3
            // 比较并且设置结点等待状态,设置为0
            compareAndSetWaitStatus(node, ws, 0);

        // 获取node节点的下一个结点
        Node s = node.next;
        if (s == null || s.waitStatus > 0) { // 下一个结点为空或者下一个节点的等待状态大于0,即为CANCELLED
            // s赋值为空
            s = null; 
            // 从尾结点开始从后往前开始遍历
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0) // 找到等待状态小于等于0的结点,找到最前的状态小于等于0的结点
                    // 保存结点
                    s = t;
        }
        if (s != null) // 该结点不为为空,唤醒
            LockSupport.unpark(s.thread);
    }

       acquire()的过程即先调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

release函数

public final boolean release(int arg) {
        if (tryRelease(arg)) { // 释放成功
            // 保存头结点
            Node h = head; 
            if (h != null && h.waitStatus != 0) // 头结点不为空并且头结点状态不为0
                unparkSuccessor(h); //释放头结点的后继结点
            return true;
        }
        return false;
    }
       tryRelease的默认实现是抛出异常,需要具体的子类实现,如果tryRelease成功,那么如果头结点不为空并且头结点的状态不为0,则释放头结点的后继结点,unparkSuccessor函数已经分析过,不再累赘。
       对于其他函数我们也可以分析,与前面分析的函数大同小异,所以,不再累赘。

acquireShared函数

       此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是acquireShared()的源码:
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
       这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。
       所以这里acquireShared()的流程就是:tryAcquireShared()尝试获取资源,成功则直接返回;失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。

releaseShared函数

       此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。下面是releaseShared()的源码:
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {//尝试释放资源
        doReleaseShared();//唤醒后继结点
        return true;
    }
    return false;
}
       此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。


AbstractQueuedSynchronizer中还有内部类ConditionObject,表示Condition Queue的数据结构,由于篇幅原因暂不展开了。



参考阅读: http://blog.youkuaiyun.com/zero__007/article/details/51570276




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值