JAVA并发基石之AQS,不得不掌握的炼级心法

AQS是什么?

全称AbstractQueuedSynchronizer 抽象队列同步器,是java.util.concurrent.locks包下的基础组件,
是用于构建锁和同步器的框架,通俗的理解就是多线程环境下,AQS封装了一揽子对临界资源线程安全的操作,以ReentrantLock来看:
获取锁?成功则锁住,否则放入FIFO队列等待获取锁(线程挂起或者说阻塞),锁的释放等这些底层操作都由AQS封装;
许多同步器都可以通过AQS很容易并且高效的构造出来

AQS在Java并发编程中扮演什么角色?

java.util.concurrent包下许多可阻塞类的底层实现,如ReentrantLock, CountDownLatch, Semaphore,
ReentrantReadWriteLock, SynchronousQueue和FutureTask都是基于AQS构建,
可见搞懂AQS在阅读java并发包下的其他组件时是多么必要!!!

AQS核心是什么?

包含三部分核心内容,状态、队列、以及由具体工具类去实现的获取/释放等方法

  /**
    * The synchronization state.
    */
  private volatile int state;
  • state状态:如上定义, 它的含义也会由具体的实现类不同而具有不同的含义,来看看几个常用的同步器中的state含义:

    • ReentrantLock:state状态用于表示重入次数,重入1次state加1,释放一次state减1,state为零表示没有任何线程占用锁
    • Semaphore同步器:state状态用于保存当前可用许可的数量,当尝试拿一个许可并成功拿到时,相应的许可数量减去;
      如果尝试释放并成功释放许可时,许可数量将会增加
    • CountDownLatch同步器:和Semaphore和相似,state状态保存的是当前的计数值,countDown方法调用release,从而导致计数值递减,
      并且当计数值为零时,解除所有线程的阻塞;await调用acquire,当计数器为零时,acquire将立即返回,否则将阻塞
    • FutureTask:state状态保存任务的状态,如正在运行、已完成或已取消
    • ReentrantReadWriteLock:此实现有两把锁分别是读锁和写锁,state状态使用16位的状态来表示写入锁的计数,使用另外16位的状态来表示读取锁的计数
  • FIFO队列 先进先出队列

    • 主要的作用是用来存储等待的线程。比如很多线程都想要去抢夺这把锁,但是大部分都是抢不到的,该如何处理这些线程呢?
      那就将它们放进队列里面,然后进行处理
    • 当多个线程去竞争同一把锁的时候,就需要排队机制把那些没能拿到锁的线程串联起来,当前面的线程释放锁之后,这个管理器就会挑选一个合适的线程来尝试抢刚才被释放的那把锁
      所以AQS就一直维护这个队列,并把等待的线程都放进队列里面
    • 队列内部是双向链表,结构看起来简单,但是要线程安全的维护这个队列,也着实不容易…
  • 获取/释放方法

    • AQS使用模版方法模式,将tryAcquire,tryRelease等具体获取和释放资源的方法提供给子类实现,这里的资源主要针对的是state值的处理,来看一个例子就明白了
     /**
      * <p>Here is a non-reentrant mutual exclusion lock class that uses
      * the value zero to represent the unlocked state, and one to
      * represent the locked state. While a non-reentrant lock
      * does not strictly require recording of the current owner
      * thread, this class does so anyway to make usage easier to monitor.
      * It also supports conditions and exposes
      */
     class Mutex implements Lock, java.io.Serializable {
     
         // Our internal helper class
         // 这里去继承AQS并实现相关方法
         private static class Sync extends AbstractQueuedSynchronizer {
             // Reports whether in locked state
             @Override
             protected boolean isHeldExclusively() {
                 return getState() == 1;
             }
     
             // 使用者实现
             // Acquires the lock if state is zero
             @Override
             public boolean tryAcquire(int acquires) {
                 assert acquires == 1; // Otherwise unused
                 if (compareAndSetState(0, 1)) {
                     setExclusiveOwnerThread(Thread.currentThread());
                     return true;
                 }
                 return false;
             }
     
             // 使用者实现
             // Releases the lock by setting state to zero
             @Override
             protected boolean tryRelease(int releases) {
                 assert releases == 1; // Otherwise unused
                 if (getState() == 0) {
                     throw new IllegalMonitorStateException();
                 }
                 setExclusiveOwnerThread(null);
                 setState(0);
                 return true;
             }
     
             // Provides a Condition
             Condition newCondition() { return new ConditionObject(); }
     
             // Deserializes properly
             private void readObject(ObjectInputStream s)
                     throws IOException, ClassNotFoundException {
                 s.defaultReadObject();
                 setState(0); // reset to unlocked state
             }
         }
     
         // The sync object does all the hard work. We just forward to it.
         private final Sync sync = new Sync();
     
         @Override
         public void lock()                { sync.acquire(1); }
         @Override
         public boolean tryLock()          { return sync.tryAcquire(1); }
         @Override
         public void unlock()              { sync.release(1); }
         @Override
         public Condition newCondition()   { return sync.newCondition(); }
         public boolean isLocked()         { return sync.isHeldExclusively(); }
         public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
         @Override
         public void lockInterruptibly() throws InterruptedException {
             sync.acquireInterruptibly(1);
         }
         @Override
         public boolean tryLock(long timeout, TimeUnit unit)
                 throws InterruptedException {
             return sync.tryAcquireNanos(1, unit.toNanos(timeout));
         }
     
     }
     //
  

上面通过继承AbstractQueuedSynchronizer实现的非重入独占锁,state=0表示可以获取锁,state=1表示次锁已经被持有了,
可以看见通过重写tryAcquire,tryRelease方法对具体资源的处理

AQS核心原理

  • state:状态量 也就是资源,实际获取、释放都是通过围绕state状态量判断

    private volatile int state;
    
  • 等待队列:实际是通过双向链表实现的队列,分别有队头引用head, 队尾引用tail

    • head: 特别需要注意的是, head对应的线程已经是获得了资源,因此我们常说的唤醒是针对的除head节点之外的其他节点
    • 等待队列:强调上一条,真正等待唤醒的节点是除head之外的其他节点
    • 等待队列中被唤醒的节点只有获取资源成功了才能设置成head
    • 同时需要注意的是,只有前驱节点是head的节点才有机会尝试获取资源
    final Node p = node.predecessor();
    if (p == head && tryAcquire(arg))
  • AQS大致的思路:定义state资源,作为能否获取成功的依据,如果获取失败(那也得想办法处理不是?),就放进AQS维护的一个等待队列,
    等待资源释放了,就尝试唤醒后继节点(一般是这样,但如果是cancel的需要排除队列),被唤醒的节点尝试去获取资源,这个时候也可能会失败的哈,
    比如ReentrantLock的非公平锁实现中,每次tryAcquire都要尝试去获取资源(这里就和等待队列里被唤醒的节点是竞争关系),通过CAS+自旋的方式保证线程安全性

  • Node节点定义:

         static final class Node {
             // 表示当前节点在共享模式上等待
             static final Node SHARED = new Node();
             // 表示当前节点在独占模式上等待
             static final Node EXCLUSIVE = null;

             // waitStatus有5个值,初始值=0
             // CANCELLED =  1
             // SIGNAL    = -1
             // CONDITION = -2
             // PROPAGATE = -3
             volatile int waitStatus;
             
             volatile Node prev; // 前驱节点
     
             volatile Node next; // 后继节点
     
             volatile Thread thread; // 此节点对应的线程
     
             Node nextWaiter; 
             
             ....
  • SIGNAL = -1,表明后继节点需要被当前节点唤醒
  • CANCELLED = 1,由于超时或中断,该节点被取消,节点永远不会离开此状态。特别的是,具有取消节点的线程永远不会再次阻塞
  • CONDITION = -2,表示结点线程等待在condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁
  • PROPAGATE = -3,共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能唤醒后继节点的后继节点
  • head,tail节点:分别是队头,队尾节点
         /**
          * Head of the wait queue, lazily initialized.  Except for
          * initialization, it is modified only via method setHead.  Note:
          * If head exists, its waitStatus is guaranteed not to be
          * CANCELLED.
          */
         // 等待队列头节点,只能通过方法setHead设置head,如果head节点存在,
         // 它的waitStatus状态一定不会是CANCELLED 
         private transient volatile Node head;
     
         /**
          * Tail of the wait queue, lazily initialized.  Modified only via
          * method enq to add new wait node.
          */
         // 等待队列尾节点,只会在enq方法中添加到队列
         private transient volatile Node tail;   

源码分析

独占模式

1、acquire方法

忽略中断的独占模式的入口方法

  • 所谓忽略中断,就是当线程发生中断的情况仅仅只是记录中断状态,而不会类似于抛出中断异常来响应,具体响应中断可由实现类处理
    // arg参数表示的需要获取的资源量
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

主要操作:

  • 首先通过tryAcquire尝试获取资源,如果获取成功这里就结束了;tryAcquire方式使用的模版方法模式来设计,由实现类去实现具体如何获取资源这个操作
  • 如果获取失败了,通过addWaiter方法把当前线程封装成一个Node节点放进等待队列队尾
  • acquireQueued核心方法,通常等待队列里的节点在尝试获取锁失败之后,都会阻塞在此放方法中(park操作,也就是线程挂起),
    当节点被唤醒时,又从被挂起的地方开始尝试获取资源;因为在被阻塞的过程中,此线程可能被中断了,所以需要记录中断标志(不立即响应),交给上层处理,acquireQueued返回值就代表释放已经中断
  • selfInterrupt就是检测到中断后,设置中断标志
        // 此操作会清除中断标志
        Thread.interrupted()
        
        // 所以这里需要重新设置中断标志
        static void selfInterrupt() {
            Thread.currentThread().interrupt();
        }     

1.1 addWaiter

简单来说就是将当前线程封装成Node节点,添加至队列尾部

    /**
     * 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;
        // 如果tail为空说明队列还没有初始化,需要通过enq进行处理
        if (pred != null) {
            node.prev = pred;
            // 如果tail节点设置成功了就直接返回,否则通过enq处理(毕竟多线程环境CAS操作也有可能失败嘛)
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

1.1.1 enq方法

自循环的方式将node加入队列尾部,直到成功

    // 返回当前节点node的前驱节点
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 如果还没有tail节点,说明队列还没有初始化
            if (t == null) { // Must initialize
                // 注意这里先加入的是一个空Node节点,同时也没有直接退出enq方法
                // 也就是上面说的,除head之外 等待队列里的其他节点才是真正在阻塞状态的节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                // CAS设置成了就直接返回
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

这一通过自旋的方式保证能够将node节点添加至等待队列尾部

1.2、acquireQueued

独占不响应中断模式尝试获取资源,如果此节点node的前驱节点是head,那它才有资格去尝试获取资源,获取失败之后挂起线程,进入阻塞状态

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            // 记录中断状态,用做方法返回
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); // 前驱节点
                // 如果是前驱是head节点才去尝试获取资源
                if (p == head && tryAcquire(arg)) {
                    // 只有获取资源成功之后才重新设置head为当前节点node
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 判断获取资源失败之后应该挂起线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 挂起线程,并在被唤醒之后检查线程中断状态
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果此节点node在尝试获取资源的过程中出现了异常,那就直接从等待队列里面取消此节点
            if (failed)
                cancelAcquire(node);
        }
    }    
    //

主要操作:

  • 如果当前node节点的前驱是head节点,那么才有资格去尝试获取资源
  • 如果获取资源成功之后,才会设置head为当前节点node
  • 在获取资源失败之后一般就会考虑是否需要挂起线程
  • 挂起的线程在被唤醒之后就会继续从挂起前的地方开始操作,这个时候需要检查一下中断标志,如果中断了需要记录一下,方便返回
1.2.1、shouldParkAfterFailedAcquire

判断是否应该挂起当获取资源失败之后

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 这里是拿到的是前驱节点的状态,当前节点node需要确保前驱节点是SIGNAL状态,因为只有这样
        // 前驱节点在释放资源之后才会唤醒当前节点node,也就是当前节点才能放心的去挂起
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) { // 如果前驱节点被取消了,那不断尝试往前找未取消的前驱节点
                      // 这是一定能找到的,因为至少head节点是不可能被取消的
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            // 这里并不会直接挂起当前节点,而是设置前驱节点的waitStatus为SIGNAL,表明在它释放资源之后需要唤醒当前节点
            // 为啥不直接挂起当前节点node呢?采用的做法是在线程挂起之前再尝试一次获取资源,如果成功就不需要挂起了
            // 考虑这样一个场景:队列里有一个head和一个刚加入队列的节点a,节点a尝试过一次获取资源,但是失败了,因为head节点还没有释放资源
            // 在节点a判断是否需要挂起之前,这个时候正好head节点释放资源,这个时候节点a只要再尝试一次获取资源也就可以成功了
            // 毕竟挂起和唤醒线程的消耗是高于自旋等待的消耗
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    //

主要操作

  • 当前节点node需要确保前驱节点是SIGNAL状态后 它才放心把自己挂起(因为会有人唤醒自己嘛)
  • 需要注意的是:waitStatus大于0表示节点是取消状态,只有小于等于0才是有效状态
  • 当前驱节点的ws大于0了说明前驱节点被取消了,需要为当前节点找到一个未被取消的前驱节点,这个前驱节点一定是能找到的,至少head节点是不可能被取消的
  • 否则的话需要将前驱节点的状态设置为SIGNAL,表明前驱节点释放资源后需要通知到自己(也就是当前node节点);当前节点node再经过一轮尝试获取资源失败后就可以直接挂起了
1.2.2、parkAndCheckInterrupt

挂起当前线程,并检查中断标志

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

借助LockSupport#park将当前线程挂起,同时通过Thread.interrupted()检查当前线程的中断标志,需要注意的是,
此方法会清除中断标志,因此如果需要保存中断状态的话,在上层需要再重新设置

1.2.3、cancelAcquire

取消需要获取资源的node

    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        // 找到未被取消的前驱节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        // 如果当前节点node为tail,那就重新将pred设置为tail
        if (node == tail && compareAndSetTail(node, pred)) {
            // 设置成功之后,因为pred为tail节点,肯定是没有后继节点了
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    // 相当于将node节点从队列里面排除了
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 试想当前node节点是等待队列的第一个节点(head除外),如果它被取消了,是不是得担负起唤醒后继节点的责任?
                // 假如节点node是head节点释放资源后唤醒的一个节点,而节点node在获取资源的时候发生了异常,node节点需要被取消
                // 但是传播唤醒这个责任是不是就落到了node节点的头上? (毕竟是head唤醒了你,而你又要跑路,你是不是得先把唤醒责任处理好了再跑呢?)
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }
    //

主要操作:

  • 什么时候会被取消?从源码注释来看,当超时或者响应中断的时候会进行取消
  • 一般情况下将取消的节点从链表中移除即可
  • 如果当前node节点的前驱节点是head,自己(node)本来是被唤醒去获取资源的,现在被取消要删除了,后面的 “兄弟"谁去唤醒呢?
    因此这种情况下,我(node)需要唤醒一个后面的"兄弟”,才能安静的离去…
2、release

独占模式下的资源释放

    public final boolean release(int arg) {
        // 通过tryRelease去尝试释放资源
        if (tryRelease(arg)) {
            Node h = head;
            // head不为空说明等待队列里面有待唤醒的节点,waitStatus=0表示后继节点不需要唤醒,
            // 因此需要满足h.waitStatus!=0才能去唤醒后继节点
            if (h != null && h.waitStatus != 0)
                // 尝试唤醒后继节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

主要操作:

  • 通过tryRelease尝试去释放资源,这是利用模版方式模式,具体由子类去实现
  • 满足待唤醒条件后,通过unparkSuccessor去唤醒
2.1 unparkSuccessor

唤醒后继节点

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        // 如果ws < 0表明是正常状态,置为0表示不需要唤醒后继节点这类操作
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        // ws大于0表示已经取消了,那就找到第一个未取消的节点来唤醒,不过这里是通过从后往前扫的方式处理的
        // 通常情况下是唤醒node.next节点,但是考虑到节点被取消或者是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;
        }
        // 唤醒线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }
   

共享模式

此模式下需要搞清楚tryAcquireShared方法返回的三类数值

  • 如果返回小于0的值:表示此次获取资源失败
  • 如果返回等于0的值:表示此次获取资源成功,但是资源已经用尽了,不能向后传播唤醒操作了(因为没资源可用的,只能等到释放之后)
  • 如果返回大于0的值:表示此次获取资源成功,同时下一次也是有可能成功获取资源,唤醒操作向后传播(毕竟有资源可用嘛)

需要注意的是:获取资源成功之后会进行的setHead操作,也就是head会被改变,但并不是说之前head对应的节点已经释放资源结束了,换句话说新head节点获取资源运行的同时,旧head节点仍然可能在运作中,其携带的资源并未释放

举一个实际例子来看看:

  • 假设资源总量为10,现在有4个线程a,b,c,d想要获取资源运行,需要的资源分别是5,3,3,4
  • 这个时候假设a,b已经抢占资源并运行了,占用资源数为8,剩余资源2;需要注意的是线程a,b可不是等待队列的节点哈
  • 此时线程c来了,想要获取资源,但是 3 > 2,获取失败,进入等到队列
  • 线程d也来了,同样4 > 2获取失败,进入等待队列
  • 此时等待队列的状态:两个节点等待唤醒,head节点中的Node是通过new Node()方式的"假节点",也就是没有实际运行的线程
  • 这个时候线程a运行结束,释放资源数量为5,此时资源剩余总量为7,尝试唤醒线程c(优先队列,先唤醒最开始的嘛),
    线程c唤醒后成功获取资源,并设置head为线程c所在节点,此时资源剩余量为4;需要注意的是这里head已经变了(这里是关键),
    说明前一个被唤醒的节点已经成功获取了资源,那么很有可能还有可用资源,OK,那根据共享模式的传播特性,再去唤醒下一个共享模式的节点,
    因此线程d对应的节点也被唤醒了,并成功获取资源;这个时候在运行中的线程有b,c,d

从这个例子可以看出,它并不是盲目唤醒后继节点,而是根据head变化来判断上一个唤醒的节点是否已经成功获取了资源,如果是则说明还有资源可用
否则的话,认为没有资源可用,不在唤醒后继节点了(这里还有一个问题,如果可用资源是4,等待队列中的节点a,b所需资源分别是5,4,但由于顺序关系
节点a被唤醒,尝试获取资源失败,此时尽管b节点所需资源满足要求,但是不会唤醒b节点,传播终止…)

1、acquireShared

共享模式下获取资源

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

  • 通过tryAcquireShared方法尝试获取资源,也是模版方法,由子类具体实现
  • 如果获取失败,就尝试放进等待队列
1.1 doAcquireShared

获取资源

    /**
     * Acquires in shared uninterruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireShared(int arg) {
        // 将当前线程加入队列尾部
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 前驱节点
                final Node p = node.predecessor();
                // 同样,只有前驱节点是head的节点才有机会去获取资源
                if (p == head) {
                    // 模版方法,具体资源获取由子类实现
                    int r = tryAcquireShared(arg);
                    // 如果获取资源成功
                    if (r >= 0) {
                        // 设置head为当前节点,同时可能的话尝试唤醒后继共享模式的节点
                        // 这里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

  • 独占模式下,获取资源成功之后通过setHead改变head就可以了
  • 在共享模式下,通过setHeadAndPropagate方法更改head, 同时 如果资源剩余量r>0说明有资源可用,尝试唤醒后继共享模式下的节点去获取资源
    相比独占模式多了一个唤醒传播机制
1.1.1 setHeadAndPropagate

设置head并尝试唤醒后继节点

    // 这里参数propagate就是tryAcquireShared方法的当前资源剩余量 
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        
        // propagate > 0 说明有资源可用
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                // 尝试唤醒后继节点
                doReleaseShared();
        }
    }

1.1.2 doReleaseShared

共享模式下,唤醒后继节点以确保传播性;而独占模式的仅在资源释放时,才会去获取head节点的后继节点

    private void doReleaseShared() {
        // 这里使用自循环是保证在CAS失败之后也要进行重试
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // 如果后继节点需要唤醒
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                // 后继节点不需要唤醒的话,将h的ws设置为PROPAGATE,以确保release之后的传播性
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            
            // 这里很关键,如果head改变了就意味着上一个被唤醒的节点已经成功获得了资源
            // 也就是也许还有资源可以获取,因此尝试唤醒后继节点,以此类推...
            if (h == head)                   // loop if head changed
                break;
        }
    }        
    //

需要关注两点:

  • 使用自循环是保证在CAS失败之后也要进行重试
  • 通过head来判断被唤醒的节点是否已经获取资源(毕竟获取资源成功之后就会改变head的值);
    如果head改变了就意味着上一个被唤醒的节点已经成功获得了资源, 也就是也许还有资源可以获取,因此尝试唤醒后继节点,以此类推…
2、releaseShared 释放资源
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            // 这里也是通过doReleaseShared来释放资源
            doReleaseShared();
            return true;
        }
        return false;
    }

小结

从源码层面来看分析了独占模式和共享模式的获取、释放资源相关源码,其中还有其配套的响应中断的独占模式 和 响应中断的共享模式,
结构上基本一致,只不过在对遇到中断的情况下做了中断响应,这里不在赘述,有兴趣可以看看源码。

整个AQS的结构都是围绕着三个核心点展开

  • state状态: 不同的实现类有不同的含义,通俗的理解就是资源
  • FIFO队列: 当尝试获取资源失败之后,得想一个办法把失败的线程给存起来,这里也就是给放进队列里了;
    麻烦的是需要考虑在多线程环境下维护队列的线程安全性,同时需要剔除取消的节点,考虑如何唤醒后继节点等…
  • 获取/释放资源方法: 因为不同的实现类对资源的定义不同,因此具体的获取和释放资源的方法 通过模版方法的方式交给子类来实现

AQS在具体的工作

  • FIFO是其核心,维护队列,线程挂起、唤醒,取消节点移除、共享模式下的传播性维护等等,都是为了队列节点合理的获取资源,
    维持其正确性和高效性,保证在多线程环境下资源合理的竞争,竞争者被合理的分配去获取资源。

以上个人理解,如有问题请指出,谢谢!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柏油

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值