并发编程之——AQS原理和阻塞队列变化

1、AQS简介

2、源码分析

2.1 线程阻塞

2.2 线程唤醒


1、AQS简介

AQS全名:AbstractQueuedSynchronizer,它就是Java的一个抽象类,它的出现是为了解决多线程竞争共享资源而引发的安全问题,细致点说AQS具备一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中,队列是双向队列。

常用的实现类是ReentrantLock(重入锁),ReentrantReadWriteLock(读写锁),CountdownLatch(计数器)等等,这些实现类都是通过内部类Sync继承AbstractQueuedSynchronizer,从而实现相应功能的。

先看下这张图,对Java的重入锁的代码结构有个大概的了解。 

并发编程之——AQS原理和阻塞队列变化_第1张图片

内部抽象类Sync继承AbstractQueuedSynchronizer,ReentrantReadWriteLock,CountdownLatch都是一样;

我们一般使用AQS功能的简单代码实现:

public class Demo {
    static Lock lock = new ReentrantLock();

    public static void test() {
        lock.lock();
        try {
            // TODO:
        }
        catch (InterruptedException ex) {
        }
        finally {
            lock.unlock();
        }
    }
}

重入锁通过加锁lock 和 解锁unlock操作进行多线程的同步控制操作。从上面代码我们可以猜想到,在多线程竞争情况下,当线程加锁操作获取不到锁,则线程要进入阻塞队列;当锁释放后,队列节点(线程)要能够获得锁;那么问题来了:

1、线程获取到锁具体是怎么实现的?

2、线程获取不到锁具体是怎么操作的?

3、锁释放后,队列节点是怎么触发获取锁的?

这些细节问题,一定要看源码才能得到答案。

2、源码分析

2.1 线程阻塞

以公平锁为例:

java.util.concurrent.locks.ReentrantLock$FairSync

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1); // 父类方法,1表示一次
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获取锁的状态,其实就是计数器,0表示锁没有被任何线程获得
            int c = getState();
            if (c == 0) {
                // 这里有三个方法:
                // 1.hasQueuedPredecessors 有没有前序结点,如果有肯定轮不到当前线程,
                //   这个方法比较巧妙,需要先理解队列设计思想才能看懂,后面有图分析
                // 2.compareAndSetState 设置锁计数器=1,原子操作
                // 3.setExclusiveOwnerThread 设置独占
                // 三个操作都符合条件才算当前线程获得锁
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                // 如果当前线程已经获得锁,那么锁计数器state加1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

总结:AQS获取锁的机制就是维护一个int属性state,

  • state=0:表示锁没人在用
  • state>0:表示有线程获得锁,state的值表示线程重入的次数,即多次获得锁。

java.util.concurrent.locks.AbstractQueuedSynchronizer

    /**
     * The synchronization state.
     */
    private volatile int state;

    public final void acquire(int arg) {
        // 子类(公平/非公平)调用父类的这个方法
        // 如果tryAcquire尝试加锁成功就没有后面方法什么事了
        // 如果tryAcquire尝试加锁是失败,则先addWaiter把线程加到队列,然后再acquireQueued尝试获取锁。。。
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果线程被中断过,再调用一次interrupt方法,清楚中断状态
            selfInterrupt();
    }

    /**
     * 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 node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail; // 找尾结点,可能是null,比如:第二个线程过来
        if (pred != null) {
            // 如果排队的线程很多,给当前线程Node设置好前序结点
            node.prev = pred;
            // compareAndSetTail是将tail指向当前Node,这里是原子操作
            if (compareAndSetTail(pred, node)) {
                pred.next = node; // 给原尾结点设置后序结点,也就是当前Node
                return node; // 设置完就return
            }
        }

        // 当pred先序结点为空的时候,有可能需要初始化队列
        enq(node); 
        return node;
    }

    /**
     * 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;
            // 可以看到这里依然对tail做了if-else判断,为啥呢?
            // 因为多线程,这里有可能tail就是非空,所以上面方法说有可能初始化
            if (t == null) { // Must initialize
                // 初始化分支
                // compareAndSetHead是个原子操作,反正CAS开头的都是原子操作
                // 需要注意的是,这里不是用的方法参数node,而是先创建了一个Node,并且head,tail都指向了这个空Node
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 已经初始化分支
                // 这里把node设置成tail结点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t; //返回原来的tail,初始化的时候并不关心返回值,只在xxx的时候关心
                }
            }
        }
    }

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    // 核心方法
    final boolean acquireQueued(final Node node, int arg) {
        // 到这里,总结一下addWaiter方法
        // addWaiter方法做的事情就是把当前线程封装成一个node,加入到队列中,并返回
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 又一个死循环,直到return interrupted获取锁
            for (;;) {
                // 这个for循环的意思是:
                // 1、如果当前线程确定还轮不到获取锁,则乖乖地进入队列,并且当前线程中断
                // 2、如果当前线程可以获取锁,则死循环反复去尝试获取锁,当然,在公平锁模式下一次就OK了
                final Node p = node.predecessor(); // 取前序结点
                // 如果前序节点是head节点,则尝试获取锁,即第二次尝试获取锁(第一次是tryAcquire)
                if (p == head && tryAcquire(arg)) { // 第二次tryAcquire,也就是在入队列前再尝试一把,万一锁被释放了呢!
                    // 如果获取到锁,把head指向当前node,把初始化创建的空节点GC
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted; // 这个interrupted表示的是当前线程有没有被中断过
                }

                // shouldParkAfterFailedAcquire返回true表示前序节点还在排队,所以当前节点需要去park,进到parkAndCheckInterrupt方法
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed) // 如果上面代码没有获取锁报错,需要取消获取锁的动作
                cancelAcquire(node); // 取消
        }
    }

    // 获取锁失败后应该排队
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 走进这个方法说明前面没有获得锁
        // 先拿到前序节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 前序节点还在排队呢,所以当前节点node只能挂起,安心地去排队
            // 这里说下节点得SIGNAL状态,它的意思是如果锁被释放,应该通知SIGNAL状态的节点
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;

        // 下面的情况,node节点不需要去park,最终返回false使上层调用方法死循环直到获取锁
        // 首先是ws>0,即waitStatus=cancelled=1(看内部类:Node)
        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只能是0/-3,
            /*
             * 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设置为-1:SIGNAL,因为啥呢?
            // 看上面if (ws == Node.SIGNAL)分支,只有前序节点waitStatus=-1,当前节点才能安心地去队列等待,
            // 否则当前node会一直自旋获取锁,这显然是不合理的
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    /**
     * Convenience method to park and then check if interrupted
     * 调用底层线程挂起方法将线程挂起,并且返回挂起的状态,也就是检查一下
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 这里直接调park方法是将线程挂起
        return Thread.interrupted();
    }

总结:阻塞等待

AQS使用FIFO双向阻塞队列来保存被阻塞的线程,实现机制是,AQS通过其内部类Node封装线程,同时Node维护prev,next,waitStatus信息来实现双向队列;

针对节点的waitStatus属性(等待状态),要补充说明一下,它的取值有以下几种:

  • CANCELLED  =  1;  // 取消状态,唯一个大于0的状态,表示节点获取锁超时。例如:1,2,3三个节点按顺序获取锁,结果1正在处理业务,2,3排队等待,2先来的,等久了超时了,而3没超时,等1释放锁以后,3虽然排在2后面,但是会把CANCELLED状态的2节点删掉,让后面未取消的节点顶上来;
  • SIGNAL          = -1;  // 等待触发状态,这个状态的节点就是锁释放后需要被通知的节点
  • CONDITION   = -2;  // 等待条件状态,还没研究到,先按下不表;
  • PROPAGATE = -3;  // 状态需要向后传播
  • 0;未赋值初始化为0

下面画图来说明以下阻塞队列的初始化和变化:

1、队列初始化

此时会构造一个空节点放进队列,锁的head和tail都指向这个空节点,空节点的thread,prev,next都是null;锁第一次被获得就会构造带有一个空节点的队列,当然当前线程直接就获得锁了,而不会入到队列。

并发编程之——AQS原理和阻塞队列变化_第2张图片

2、如果有线程竞争,获取不到锁的线程就会被封装成Node节点入到队列中去,但不是替换空节点,而是跟在空节点的后面;

并发编程之——AQS原理和阻塞队列变化_第3张图片

3、现在锁释放了,节点1获得锁了,看看队列的变化,队列会把head指向节点1,原来的空节点就等着被GC,节点1的thread,prev会被置空,next不变,因为如果节点1后面还有节点2的话,next就指向节点2;

总之一句话,除了初始化存在空节点以外,队列的head节点总是最后一个获得锁的节点;

并发编程之——AQS原理和阻塞队列变化_第4张图片

2.2 线程唤醒

线程唤醒的前提当然是线程被挂起,线程挂起操作在上面源码中有贴出来,线程挂起后,线程也就阻塞在这里:

LockSupport.park(this); // 这里直接调park方法是将线程挂起

上面acquireQueued方法是AQS的核心,其线程阻塞与获取锁都在这个里面,核心思想是:

1、如果前序节点还在排队(waitStatus=-1),后续节点直接挂起;

2、如果前序节点取消了(waitStatus=1),后续节点的逻辑中会把取消的前序节点删除(递归删除);

3、如果前序节点也是刚加进来的,节点状态还没定,也没有获得锁,那么当前线程要把前序节点的waitStatus设置为-1;

——第3点要好好理解,换句话说,队列里除了最后一个节点,其他节点的状态都是由其next节点来修改的。为啥要这样做呢?这是因为挂起的线程要解除挂起状态获取锁,这需要一个状态,看下面代码分析。

java.util.concurrent.locks.ReentrantLock$FairSync

    public void unlock() {
        sync.release(1);
    }

java.util.concurrent.locks.AbstractQueuedSynchronizer

    public final boolean release(int arg) {
        // 首先是尝试释放锁,有人问了,这释放锁还需要尝试吗?又不是获取锁,还可能获取不到
        // 那确实存在释放不了的情况,什么情况呢?  那就是重入次数大于1的情况,按照重入锁的设计,重入几次就需要释放几次
        if (tryRelease(arg)) {
            // 锁释放成功,要唤醒后序挂起线程
            Node h = head; // 这里拿到head节点
            if (h != null && h.waitStatus != 0) // 判断head节点状态非0,即被修改过
                // 如果h.waitStatus = 0,表示没有后续节点,能理解不?看上面第三点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    private void unparkSuccessor(Node node) {
    // 注意参数是head节点,因为唤醒后续节点总是从head往后找
        /*
         * 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)
            // 还原head节点状态为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;
        // 校验状态,因为队列里面可能就没有其他竞争线程,或者next节点取消了
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 咋整呢?从后往前遍历,遍历到最靠前的一个状态正常的节点,这个节点就是要被唤醒的节点
            // 这里需要注意的是,这里并没有删除取消的节点,因为取消是在获得锁的逻辑里面删除的
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0) // 这里就能理解为啥都要给前序节点设置状态等于-1了吧,为啥不是-2,-3呢?因为那俩有其他用处
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread); // 唤醒线程节点
    }

java.util.concurrent.locks.ReentrantLock$Sync

        // 这里逻辑就很简单了,判断state的值即可
        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;
        }

最后再来个总结,要说AQS的原理,很多人会谈到队列,计数器,但是更为底层的支撑,我认为应该是CAS + LockSupport,如果你详细看过源码的话,会发现AQS到处都是CAS操作(CAS操作的本质就是乐观+自旋),线程的挂起和唤醒则是通过LockSupport来处理。队列只是其实现的一种方式,换句话说,即便用数组应该也能做的到。

AQS和Synchronized,要说这两者的区别,有很多,我不想一一列举了,意义不大,因为随着版本的演进,两者有很多地方都在相互靠拢。

最后谈一下LockSupport,其核心功能就park和unpark,挂起和解挂,我看有些文章说重入锁的挂起不会有线程的用户/内核态切换,这是错的,不管是ReentrantLock还是Synchronized,只要发生锁的竞争,最终都是会有线程的状态切换的(自旋失败)。

相对于wait/notify,LockSupport有很多优点,具体这里不累赘了,只举个例子吧:先做一次unpark,在做一次park,线程不会挂起;但是先做两次unpark,再做两次park,线程就会挂起。

比如说:你去澡堂洗澡,澡堂服务员要给你一个钥匙牌子挂在手上,才能进澡堂子,但是AQS是一个特殊的澡堂,它只给一个线程服务,而且它只有一个牌子,可以理解为:这个澡堂子只服务一个顾客,而且只有一个寄存柜,也就是只有一个牌子(钥匙),那么线程阻塞则好比你去洗澡,但是服务员没有牌子给你,你只能等,等那个牌子被释放,你可能问了,不是说只服务我一个顾客吗? 是的,不错,但是这个服务员缺心眼啊,它不支持重入啊,它并不会因为你已经拿到唯一的牌子了,就让你进去,所以这Java只能自己实现重入锁了!

再看下上面两个例子:

1、先做一次unpark,在做一次park

——unpark相当于洗好澡了,把牌子还回去,park相当于去洗澡,发现有牌子,直接拿着牌子去洗澡了,所以线程不会阻塞。

2、先做两次unpark,再做两次park

——做两次unpark,因为只有一个牌子,所以效果跟做一次unpark一样,接着,第一次park可以拿到牌子,第二次park就拿不到牌子了,所以线程阻塞。这里能看出来native代码傻了吧!

你可能感兴趣的:(java,AQS,重入锁)