Java之AQS(一)-CSDN博客

65 阅读5分钟

AQS


前言

AQS,抽象队列同步器,通过维护一个锁状态state和一个双向队列,为各种花里胡哨的锁(ReentrantLock,重入锁;CountDownLatch,计数器;等等)提供了一些基本的实现(如:获取锁状态;进入等待队列;CAS操作的封装;线程间的通信机制等等)。如图所示,sync和Worker都是继承于AQS。

在这里插入图片描述

在这里插入图片描述

如何使用

以一个互斥锁为例子,我们的Syn继承了抽象队列同步器并实现了其中的isHeldExclusively()方法,tryAcquire()方法及tryRelease方法这三个方法。这三个方法中,抽象队列同步器都没有为我们提供具体实现,可以结合抽象队列同步器提供的cas、状态获取等方法灵活实现。

/***
 *
 * @Author:fsn
 * @Date: 2020/4/26 18:44
 * @Description
 */


public class Mutex implements Lock {

    private final static int expect = 0;
    private final static int update = 1;

    private Syn syn = new Syn();

    @Override
    public void lock() {
        syn.acquire(update);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        syn.acquireInterruptibly(update);
    }

    @Override
    public boolean tryLock() {
        return syn.tryAcquire(update);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return syn.tryAcquireNanos(update, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        syn.release(update);
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    private static class Syn extends AbstractQueuedSynchronizer {
        // 1、AQS内部使用一个int成员变量代表同步状态
        //2、AQS通过内置的FIFO队列来完成线程的排队工作

        @Override
        protected boolean isHeldExclusively() {
            // 是否保持独占
            return getState() == 1;
        }

        @Override
        protected boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 该状态表示未有锁竞争出现
            if (c==0) {
                // 进行CAS操作
                if (compareAndSetState(expect, update)) {
                    // cas成功, 设置为独占
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
//          这里也可以模仿重入锁进行实现
//          else if (current == getExclusiveOwnerThread()) {
//                int nextc = c + acquires;
//                if (nextc < 0)
//                    throw new Error("Maximum lock count exceeded");
//                setState(nextc);
//                return true;
//            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            // 判断当前线程是否为独占的线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 这里模拟的是重入锁的实现方式,其实这里可以直接判断
            // getState方法是否为1,表示是否为独占状态
            int c = getState() - arg;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
    }
}

其中,isHeldExclusively()方法,用于判断线程是否为独占,你可以根据状态进行判断,也可根据当前线程是否和独占线程进行判断。这个状态根据你加锁时的设置而决定的,它作为一个int类型的成员变量,默认值即为0,一般情况下都习惯用0代表为无锁,1代表加锁。

 protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

tryAcquire(),申请锁方法也是没有进行实现的,我们可以自己灵活实现,以文中互斥锁的例子,从抽象队列同步器里头获取当前的状态,为0表示没有加锁,此时调用抽象队列同步器的compareAndSetState方法进行CAS操作。cas操作采用Unsafe工具进行实现,其中compareAndSwapInt方法是一个用native修饰的本地方法。cas成功后,将当前线程设置为独占的线程。

 protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

分析

acquire

经过上面的例子,你可能对AQS有一个基本了解了,但这不还不是其强大的地方。如果有留意过重入锁里头的lock方法的实现,你会发现它申请加锁时还会调用抽象队列同步器里头的acquire()方法,通过注释,我们可以明白此方法是独占模式下线程获取共享资源的入口,一开始还是会调用tryAcquire方法,如果获取到了资源则直接返回,否则进入等待队列,直至获取到资源为止。且这个过程忽略中断的影响,如下代码所示。

 /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        // 如果申请锁不成功, 则放入阻塞队列,这里还是会调用tryAcquire()方法
        // 该方法也是我们自定义实现的
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

addWaiter

首先关于addWaiter()添加等待者方法, 该方法如下代码所示,会创建一个 给定模式的等待者。结合addWaiter(Node.EXCLUSIVE),我们可以找知道该方法创建了一个 标记为独占的节点,并进行入队操作。

  /**
      * 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
         // 获取队列的尾部,不为null, 则把当前节点的上一个节点设置为尾部节点
         Node pred = tail;
         if (pred != null) {
             node.prev = pred;
             // 只有cas成功, 才能将尾部节点的下一个节点指向当前节点
             // 这里cas操作企图将当前节点设置为尾部节点
             if (compareAndSetTail(pred, node)) {
                 pred.next = node;
                 return node;
             }
         }
         // 如果队列为空或者cas失败则进入这个方法, 该方法源码
         // 在模式定义源码的后边
         enq(node);
         return node;
     }

这里的模式应该就是等待状态的意思,它的几种状态如下所示,通过查看下方代码,关于EXCLUSIVE的含义如下,注意这里它这里申明是null的。


static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        // 标明一个节点正在等待共享锁
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        // 表明一个节点正在等待独占锁
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        // 表明一个等待的线程被取消了
        static final int CANCELLED =  1;

        /** waitStatus value to indicate successor's thread needs unparking */
        // 标明一个等待线程的下一个线程需要被唤醒
        static final int SIGNAL    = -1;

        /** waitStatus value to indicate thread is waiting on condition */
        // 当前线程正在等待中
        static final int CONDITION = -2;

        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        // 下一次的acquire方法应该被无条件的传播
        static final int PROPAGATE = -3;

        /**
         * Status field, taking on only the values:
         *   SIGNAL:     The successor of this node is (or will soon be)
         *               blocked (via park), so the current node must
         *               unpark its successor when it releases or
         *               cancels. To avoid races, acquire methods must
         *               first indicate they need a signal,
         *               then retry the atomic acquire, and then,
         *               on failure, block.
         *   CANCELLED:  This node is cancelled due to timeout or interrupt.
         *               Nodes never leave this state. In particular,
         *               a thread with cancelled node never again blocks.
         *   CONDITION:  This node is currently on a condition queue.
         *               It will not be used as a sync queue node
         *               until transferred, at which time the status
         *               will be set to 0. (Use of this value here has
         *               nothing to do with the other uses of the
         *               field, but simplifies mechanics.)
         *   PROPAGATE:  A releaseShared should be propagated to other
         *               nodes. This is set (for head node only) in
         *               doReleaseShared to ensure propagation
         *               continues, even if other operations have
         *               since intervened.
         *   0:          None of the above
         *
    
        
        // 以下省略
   }

enq

enq()源码如下,这里搞了一个死循环(自旋),如果第一次cas失败会一直在这里轮询, 直到成功。

  /**
       * Inserts node into queue, initializing if necessary. See picture above.
       * @param node the node to insert
       * @return node's predecessor
       */
      private Node enq(final Node node) {
          for (;;) {
              Node t = tail;
              if (t == null) { // Must initialize
                  if (compareAndSetHead(new Node()))
                      tail = head;
              } else {
                  // 新的阶段的上个节点指向尾部节点, 就是尾插法
                  node.prev = t;
                  // cas成功, 再指向新节点. 双向链表
                  if (compareAndSetTail(t, node)) {
                      t.next = node;
                      return t;
                  }
              }
          }
      }

acquireQueued

再回到这句代码acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),我们可以知道它的含义就是,创建一个标记为独占的节点,然后入队。入队之后我们得进行处理吧?不能让每个线程就这样干等着吧。所以acquireQueued就是对队列中的线程进行相关操作。源码如下:

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;
                }
                // 判断获取锁失败之后是否可以进入等待唤醒状态
                // 该方法保证当前线程的前驱节点的waitStatus属性值为SIGNAL,
                // 从而保证了自己挂起后,前驱节点会负责在合适的时候唤醒自己。
                if (shouldParkAfterFailedAcquire(p, node) &&
                        // 用于挂起当前线程,并检查中断状态
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

源码中,if (p == head && tryAcquire(arg)),为什么前驱节点为头结点就是尝试获取锁呢?根据enq中的源码,它其实就是一个无效节点,既然无效,当前节点理所当然可以获取锁。

 if (compareAndSetHead(new Node()))
              tail = head;

获取锁成功后,将当前节点设置为头节点,看看设置源码,其实就是类似于出队的操作,因为当 前节点获取锁成功了,就应该继续执行临界资源了,就把它踢出去了。

   /**
      * Sets head of queue to be node, thus dequeuing. Called only by
      * acquire methods.  Also nulls out unused fields for sake of GC
      * and to suppress unnecessary signals and traversals.
      *
      * @param node the node
      */
     private void setHead(Node node) {
         head = node;
         node.thread = null;
         node.prev = null;
     }

shouldParkAfterFailedAcquire

再来看看shouldParkAfterFailedAcquire(p, node)方法和parkAnd CheckInterrupt()方法,其中,shouldParkAfterFailedAcquire涉及到了几种状态的转换,再开始之前回顾一下它几种状态的含义:

// 表明一个等待的线程被取消了
        static final int CANCELLED =  1;

        /** waitStatus value to indicate successor's thread needs unparking */
        // 标明一个等待线程的下一个线程需要被唤醒
        static final int SIGNAL    = -1;

        /** waitStatus value to indicate thread is waiting on condition */
        // 当前线程正在等待中
        static final int CONDITION = -2;

        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        // 下一次的acquire方法应该被无条件的传播
        static final int PROPAGATE = -3;

shouldParkAfterFailedAcquire方法有两个作用,(1)判断前驱节点等待状态是否被取消了,如果是,需要移动节点;(2)尝试将SIGNAL之外的有效状态置成SIGNAL状态

 /**
      * Checks and updates status for a node that failed to acquire.
      * Returns true if thread should block. This is the main signal
      * control in all acquire loops.  Requires that pred == node.prev.
      *
      * @param pred node's predecessor holding status
      * @param node the node
      * @return {@code true} if thread should block
      */
     private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
         // 获取前驱节点的等待状态
         int ws = pred.waitStatus;
         // 标明一个等待线程的下一个线程需要被唤醒, 确保前驱节点可以适当时机唤醒下一个节点
         if (ws == Node.SIGNAL)
             /*
              * This node has already set status asking a release
              * to signal it, so it can safely park.
              */
             return true;
          // 大于0的状态只有取消cancel一种,所以将当前节点前移直到前驱是有效节点处
         if (ws > 0) {
             /*
              * 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.
              */
             // 尝试将SIGNAL之外的有效状态置成SIGNAL状态
             compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
         }
         // 没有机会设置成SIGNAL, 继续acquireQueued的自旋操作
         return false;
     }

parkAndCheckInterrupt

parkAndCheckInterrupt()方法用于挂起当前线程,并检查中断状态,注意Thread.interrupted()并不是中断线程的含义,只是判断是否被中断过。

 /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

cancelAcquire

接下来,如果是tryAcquire()抛异常了, 我们最终要取消申请锁。然而,取消的方式也不是一个简单过程,我们需要先找到前驱没有处于取消状态的节点,然后要把当前节点设置为取消的状态。接着,如果待取消的节点是尾部节点则比较好处理,只需要把找好的前驱节点的下一个节点设置为null,如果是中间的节点,还得找一下继任者,让前驱节点连上继任者。详细信息可以看注释。

 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;
         // 如果前驱节点也是大于0表示取消状态, 则一直向前找
         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.
         if (node == tail && compareAndSetTail(node, pred)) {
             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 &&
                     // 前驱节点的等待状态处于SIGNAL状态
                 ((ws = pred.waitStatus) == Node.SIGNAL ||
                         // 或者处于其他有效状态, 尝试将前驱结点设置为SIGNAL
                  (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                     // 前驱节点的线程不为null
                 pred.thread != null) {
                 // 获取当前要取消申请锁的节点的下一个节点
                 Node next = node.next;
                 if (next != null && next.waitStatus <= 0)
                     // 将事先找好的前驱节点的下一个节点设置为当前取消申请锁的节点的下一个节点
                     compareAndSetNext(pred, predNext, next);
             } else {
                 // 释放之前唤醒继任者
                 unparkSuccessor(node);
             }
             // 当前取消申请锁的节点的下一个节点设置为自身, 表示没有被其他地方引用了
             node.next = node; // help GC
         }
     }

acquire总结

至此,我们重新回到原点,当申请锁不成功, 则放入阻塞队列,放入的过程中,我们已经知道会返回一个中断状态表示是否被中断,如果为true,这里selfInterrupt()才进行中断操作。

 public final void acquire(int arg) {
        // 如果申请锁不成功, 则放入阻塞队列,这里还是会调用tryAcquire()方法
        // 该方法也是我们自定义实现的
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
  /**
      * Convenience method to interrupt current thread.
      */
     static void selfInterrupt() {
         Thread.currentThread().interrupt();
     }

总结

本文通过一个使用AQS的例子,分析了AQS所提供的一些基本操作,然后以acquire()方法为切入点逐步分析了一个线程申请锁时经历的步骤。
其中:

- addWaiter负责将当前等待锁的线程包装成Node,并添加到队列的末尾,enq方法通过自旋方式确保入队成功,同时,enq方法同时还负责在队列为空时初始化队列。
- acquireQueued方法用于在Node成功入队后,继续尝试获取锁(当Node的前驱节点是head时才能获取,这也符合了队列设置的规则FIFO)或者将线程挂起。
- shouldParkAfterFailedAcquire方法用于保证当前线程的前驱节点的waitStatus属性值为SIGNAL(如果状态不为SIGNAL,则会尝试将其他有效状态改变为SIGNAL),从而保证了自己挂起后,前驱节点会负责在合适的时候唤醒自己。
- parkAndCheckInterrupt方法用于挂起当前线程,并检查中断状态。
- 如果获取锁的过程出现异常,则调用cancelAcquire方法取消获取。

结束语

本文只分析了AQS中线程入队及入队后的操作(独占锁的获取),但这只是冰山一角,AQS还提供了Condition的操作,满足我们在不同的条件下,让不同的线程可以通过signal/await等方法相互协作。这里计划放到下篇文章进行分析。