15.AQS

可以说,AQS是Java1.5出现的java.util.concurrent包的基础。

java.util.concurrent包中有许多常用的同步工具类,比如ReentrantLock、ReentrantReadWriteLock、CountdownLatch、CycleBarrier以及Semaphore,这些工具类的实现基础都是AQS。

AQS的原理是并发的线程访问的对象(实际上就是并发访问的一个锁对象)有一个共享的state变量,依据锁的语义(独占还是共享)来定义这个state的值的含义。所有需要访问锁对象的线程,首先要判断这个state对象的值,获取锁的线程,执行lock成功,进入锁保护的代码;不能获取锁的线程,等待锁的释放。

实际上这个原理和sychronized关键字,以及操作系统层面的信号量的原理都是一致的,即“查询一个锁标志位”,只是细节上不太一样。

sychronized是操作系统的monitor(管程)的java实现,AQS是操作系统的信号量的java实现。

共享锁和独占锁

独占锁实际上也是一种共享锁,只不过是只能被一个线程“共享”的特殊的共享锁,实际上就是二元信号量和广义信号量的关系。

  • 获取锁的流程
    进入lock.lock()方法(lock对象保护临界区代码),在lock方法中线程安全地检查state值,判断能否获取(共享锁和独占锁的判断逻辑不同),能则线程安全地更新status(使用CAS指令),否则进入同步队列等待。

  • 释放锁的流程
    进入lock.unlock()方法,检查state,cas更新state值,当前线程状态修改,唤醒同步队列中的线程。

获取和释放就是信号量的增(P)和减(V)操作,state就是线程间共享的信号量。

AQS的独占锁获取流程

方法调用

  • 核心方法:acquire(int arg)
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  1. 如果通过tryAcquire获取锁成功,则不会进入if分支,直接进入临界区执行代码,acquire执行结束。
    tryAcquire()需要由实现类根据独占锁的获取的具体逻辑来实现。
    如ReentrantLock类的tryAcquire:
// 非公平锁的tryAcquire的内部调用方法,参数是1。(公平锁和非公平锁的区别在于,试图获取公平锁的线程会先检查锁的同步队列,如果同步队列有线程在等待,就不会尝试获取而是直接加入同步队列)
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // state==0 是未被其他线程获取
            if (c == 0) {
                // cas更新state
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 锁重入,state递增,作为重入次数
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
        // 公平锁版本:
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 即使锁未被锁定,也不会去获取,所以效率相对非公平锁会低
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    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;
        }
  1. 如果tryAcquire返回false,说明锁不可用,需要等待释放。执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法将当前线程封装成一个具有waitstatus的Node对象并加入同步队列:

addWait将线程加入同步队列,并返回线程的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;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 检查队列是否初始化,并确保尾节点是当前线程Node
        enq(node);
        return node;
    }
       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;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

线程加入队列之后,线程对象虽然在队列里,但是线程本身依然是活动的,因为并没有任何动作使得线程阻塞或者挂起或者任何方式无法继续执行,acquireQueued(Node,args)方法就是线程在同步队列中表现的行为:

  1. 获取节点的前驱节点p,如果前驱节点是头节点,就重试获取锁 ,如果节点acquire成功,那么setHead方法,将当前节点作为head、将当前节点中的thread设置为null、将当前节点的prev设置为null,这保证了数据结构中头结点永远是一个不带Thread的空节点
  2. 如果当前节点不是前驱节点或者是头结点但是tryAcquire失败,那么判断在acquie失败后是否应该park,如果判断应当park,那么parkAndCheckInterrupt方法将使用LockSupport将线程阻塞并检查中断状态。
    // 自旋等待或阻塞或尝试获取锁并返回线程的中断状态
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 注意这个for循环以及循环退出条件
            for (;;) {
                //  获取当前线程node对象的前驱节点
                final Node p = node.predecessor();
                // 如果前驱节点是头节点,就重试获取锁
                if (p == head && tryAcquire(arg)) {
                    // 如果获取成功,将此线程设置为队列的头结点
                    /*
                     private void setHead(Node node) {
                        head = node;
                        node.thread = null;
                        node.prev = null;
                    }
                    */
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) //挂起线程,唤醒之后会继续判断锁状态进入自旋或者挂起
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


为什么是前驱是头结点就重试获取锁呢?
1.一个线程节点从同步队列中获取到锁之后,线程会继续执行临界区代码,此线程节点会被setHead,由setHead方法可知,head是一个空节点,作为标志节点,且setHead这一步实际上是销毁Node对象(因为线程已经成功获取锁了,没有继续保留这个Node的意义。
2.FIFO原则,既然head是空的节点,自然要从head之后的节点开始尝试“出队”;

  • shouldParkAfterFailedAcquire(p, node)和parkAndCheckInterrupt()

waitStatus的值

  • CANCELLED(1):该节点的线程可能由于超时或被中断而处于被取消(作废)状态,一旦处于这个状态,节点状态将一直处于CANCELLED(作废),因此应该从队列中移除.
  • SIGNAL(-1):当前节点为SIGNAL时,后继节点会被挂起,因此在当前节点释放锁或被取消之后必须被唤醒(unparking)其后继结点.
  • CONDITION(-2) 该节点的线程处于等待条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal),设置其值为0,重新进入阻塞状态.
  • PROPAGATE(-3) 指示处于共享状态下,下一次的acquireShared需要无条件的传播
  • 0:新加入的节点
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;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             *它的前驱节点的waitStatus>0,相当于CANCELLED(因为状态值里面只有CANCELLED是大于0的),那么CANCELLED的节点作废,当前节点不断向前找并重新连接为双向队列,直到找到一个前驱节点waitStats不是CANCELLED的为止
             */
            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.
             */
             // 不会是-2,-2的线程处于等待条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal),设置其值为0,重新进入阻塞状态.
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

   
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

    private final boolean parkAndCheckInterrupt() {
        //  将当前线程阻塞,需要unpark来唤醒
        LockSupport.park(this);
        return Thread.interrupted();
    }

Doug Lea在设计AQS的线程阻塞策略使用了自旋等待和挂起两种方式,通过挂起线程前的低频自旋保证了AQS阻塞线程上下文切换开销及CUP时间片占用的最优化选择。保证在等待时间短通过自旋去占有锁而不需要挂起,而在等待时间长时将线程挂起。实现锁性能的最大化。

进入同步队列的线程要么短时间自旋获取到锁,执行线程,要么被系统挂起,等待被唤醒后继续执行acquireQueued内的轮询,如果没有被取消,则继续自旋或挂起,直到获取锁。

AQS的独占锁释放流程

方法调用

  • 核心方法:release(int arg)
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;  // 当前的线程节点是同步队列的头结点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);// 唤醒后继节点
            return true;
        }
        return false;
    }

tryRelease()由实现类根据锁释放逻辑处理state值。

  1. 如果当前线程通过tryRelease方法执行成功,则唤醒此节点的后继节点(获取锁的节点是同步队列的头结点,可以查看tryAcquire的逻辑)。
    如ReentrantLock的锁的释放(非公平和公平锁使用的是同一个tryRelease方法):
        protected final boolean tryRelease(int releases) {
            // 信号量递减操作
            int c = getState() - releases;
            // 判断释放锁的线程是否是占有锁的线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

唤醒后继节点的方法unparkSuccessor(head):

    // 这里在实际执行时传入的是head节点,也就是要释放锁的线程节点。
   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;
        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.
         */
        // 获取后继节点,也就是要唤醒的节点
        Node s = node.next;
        // 如果后继节点是null或者被取消了(ws==1),则从队尾回溯找到离head最近的非null且ws<=的节点
        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);
    }
  1. 头节点的waitStatus<0,将头节点的waitStatus设置为0
  2. 拿到头节点的下一个节点s,如果s==null或者s的waitStatus>0(被取消了),那么从队列尾巴开始向前寻找一个waitStatus<=0的节点作为后继要唤醒的节点
  3. 如果拿到了一个不等于null的节点s,就利用LockSupport的unpark方法让它取消阻塞。

独占锁获取和释放总结

锁获取和锁释放便构成了同步语义。锁获取中,等待获取锁的线程构建了同步队列,在同步队列中执行短时自旋然后转为阻塞状态等待被释放锁的线程唤醒;释放锁的线程作为同步队列中的头结点,唤醒其后继的等待唤醒的线程节点。唤醒的线程节点会在同步队列中重试获取锁(acquireQueued()方法),直到获取锁。

在示例API中,线程获取锁以及进入同步队列以及被阻塞时,没有做出对interrupt的响应,只是返回线程的中断状态,如果需要对中断进行响应,可以使用AQS的Interruptable的API。

独占锁使用的Node对象的waitStatus没有使用CONDITION和PROPGATE,这两个前者是留给条件对象后者是共享锁使用的。
在释放锁唤醒线程时,释放的是当前线程节点的后继节点,当然这个后继节点是有状态要求的,但是从FIFO原则来看,是首先唤醒先进入同步队列的一个线程。

共享锁的获取

acquireShared(int arg)

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) // <0 说明不能获取锁,加入同步队列
            doAcquireShared(arg);
    }
    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) {
                    int r = tryAcquireShared(arg);
                    //注意上面说的, 等于0表示不用唤醒后继节点,大于0需要
                    if (r >= 0) {
                        //这里是重点,获取到锁以后的唤醒操作 
                        setHeadAndPropagate(node, r);
                        p.next = null;
                        //如果是因为中断醒来则设置中断标记位
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //挂起逻辑跟独占锁一样 
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //获取失败的取消逻辑跟独占锁一样 
            if (failed)
                cancelAcquire(node);
        }
    }
      //两个入参,一个是当前成功获取共享锁的节点,一个就是tryAcquireShared方法的返回值,注意上面说的,它可能大于0也可能等于0
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; //记录当前头节点
        //设置新的头节点,即把当前获取到锁的节点设置为头节点
        //注:这里是获取到锁之后的操作,不需要并发控制
        setHead(node);
        //这里意思有两种情况是需要执行唤醒操作
        //1.propagate > 0 表示调用方指明了后继节点需要被唤醒
        //2.头节点后面的节点需要被唤醒(waitStatus<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();
        }
    }

tryAcquireShared方法由子类实现,共享还是独占,主要有state变量和tryAcquireShared方法来决定,如果在tryAcquireShared里面定义的是二元信号量,那么他也是独占锁。tryAcquireShared方法返回值不小于0则说明获取共享锁成功,否则进入doAcquireShared(),doAcquireShared()和独占锁的acquireQueued相似,将线程加入同步队列,自旋或阻塞等待。

获取锁的过程:

  1. 当线程调用acquireShared()申请获取锁资源时,如果成功,则进入临界区。
  2. 当获取锁失败时,则创建一个共享类型的节点并进入一个FIFO等待队列,然后被挂起等待唤醒。
  3. 当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则唤醒后面还在等待的共享节点并把该唤醒事件传递下去,即会依次唤醒在该节点后面的所有共享节点,然后进入临界区,否则继续挂起等待。 即传播的意义。
    与独占锁释放不同的是,共享锁的释放会唤醒队列中所有的节点。且唤醒操作doReleaseShared在releaseShared和setHeadAndPropagate(node, r)中都会执行。也就是说一个线程在成功释放锁和成功获取锁的时候,都会去传播式地唤醒队列中的节点。

共享锁释放

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) { // 释放成功
            doReleaseShared();   // 传播式唤醒线程
            return true;
        }
        return false;
    }

tryReleaseShared也是子类实现的模板方法。如果当前线程释放锁成功,则执行doReleaseShared(),唤醒同步队列中的线程;
这个方法唤醒会waitStatus==-1的头结点的后继(同时将head的ws设置为0,是为了防止多次唤醒后继),是如何达到传播的呢?

  1. 首先,如果没有线程等待在队列中,就没有队列,head==null,此方法返回;
  2. 如果线程执行此方法时,存在同步队列,那么队列的head节点的ws一定为-1(shouldParkAfterFailedAcquire()中的行为,只有设置了前驱会唤醒自己,才敢安心地被挂起,且不是从同步队列醒来并成功获取锁的线程是不会成为head节点的),所以一定会唤醒一个head的后继节点,执行唤醒之后,如果head没有并发改变,就break;如果head改变,执行步骤3;
  3. 传播的唤醒是在与head在退出for循环之前变化了,说明队列中有线程获取到了锁,那么执行循环,唤醒当前head的后继节点(执行步骤2)。
  4. break退出循环的线程,锁释放的动作即完成了,它和锁的关系也就终止了。接下来是就是其他线程的释放和传播式唤醒。

    private void doReleaseShared() {
        for (;;) {
            //唤醒操作由头结点开始,注意这里的头节点已经是上面新设置的头结点了
            //其实就是唤醒上面新获取到共享锁的节点的后继节点
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //表示后继节点需要被唤醒
                if (ws == Node.SIGNAL) {
                    //这里需要控制并发,因为共享锁会有多个线程能执行releaseShared
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;      
                    //执行唤醒操作      
                    unparkSuccessor(h);
                }
                //如果后继节点暂时不需要唤醒,则把当前节点状态设置为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(介入).
                 * 意思是仅仅会在此方法里面将一个head节点的ws设置为PROPGATE,目的是确保传播能够继续?
                 * 下面的代码中 在更新ws为PROPGATE失败的时候continue,更新成功就退出分支
                 * 在什么情况下head的ws会是0呢,新创建一个节点时ws=0,在队列中被唤醒称为头结点时,后继节点在阻塞之前会头节点ws置位-1才会park,
                 * 所以只会是队列中的** 最后一个节点,没有后继,无需再唤醒后继了。
                 */
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            //如果头结点没有发生变化,表示设置完成,退出循环
            //如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试
            if (h == head)                   
                break;
        }
    }

你可能感兴趣的:(15.AQS)