从AbstractQueuedSynchronizer到ReentrantLock中独占锁、公平锁、非公平锁源码分析

1. 整体介绍

AbstractQueuedSynchronizer这个类可以理解为是一个同步器,ReentrantLock、ReentrantReadWriteLock都是基于它来实现锁的获取、释放,和synchronized关键字不同的是,AbstractQueuedSynchronizer直接在代码层面实现了锁机制。

2. AbstractQueuedSynchronizer分析

2.1 独占锁获取

首先看到 acquire(int arg) 方法。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

结合注释看这个方法的作用,在排他模式下获取锁,忽略中断。调用一次 tryAcquire(int arg) 方法来返回成功或失败,如果返回true,方法直接结束,如果返回false,即获取锁失败,调用 addWaiter 方法来增加一个排他模式的Node节点,并且让这个Node节点进行排队等待,acquireQueued可以让Node进行排队,如果在排队过程中,发现线程被打断,也会进行继续排队,当被打断的线程排完队之后(获取到锁了),如果发现线程被打断,调用当前线程的 interrupted() 方法,确保线程的终端标志是“中断”状态。此方法为final类型,不允许重写,抽象方法 tryAcquire 需要留给子类实现。

然后我们看一下具体是来怎么添加Node节点的,也就是 addWaiter() 方法。

    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;
            }
        }
        enq(node);
        return node;
    }

这里先简单描述一下AbstractQueuedSynchronizer内部维护的一个队列。AbstractQueuedSynchronizer.head指向当前队列的头部的Node,这个头部Node的成员变量next指向当前队列中的第二个Node,第二个Node的next指向第三个Node,第二个Node的成员变量 prev 指向头部Node...AbstractQueuedSynchronizer.tail指向队列的尾Node。如下图所示。

首先为当前线程、模式创建一个Node节点,然后获取tail尾节点,如果尾节点不为null,将新创建的node节点的prev(prev属性指向的是上一个节点)设置为原先的尾节点,然后采用CAS的乐观机制将同步器的尾节点设置为新创建的节点,再将原来的为节点的next执行新创建的尾节点,以此来达到添加尾节点到队列的操作。这里为了防止多线程下添加尾节点顺序混乱,采用了 compareAndSet 的方法来进行操作。如果compareAndSet 未设置成功,则进入 enq() 方法来进行尾节点的添加,这个enq()方法可以理解为一个担保机制,当compareAndSet 乐观设置失败,就会触发,让 enq() 来完成。

然后我们进入enq()方法。

    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;
                }
            }
        }
    }

这里其实就是一个循环设置尾节点的操作。首先取到当前的尾节点,如果当前尾节点为null,说明此时队列是空的,这里会进行实例化一个node,将这个node设置为头结点,并让尾节点指向头结点,也就是说此时头和尾指向都是同一个结点。然后我们看非null的情况,取到方法参数node(这个node是我们要设置成尾节点的node),还是通过 compareAndSet 的方法来进行尾节点的设置操作,如果compareAndSet 成功,再将原先的尾节点的next指向新的尾节点,然后直接返回;如果设置失败了,还是继续for循环,继续设置,直到成功,然后返回。这么做的目的是为了保证在多线程的环境下,尾节点能够被依次按顺序地添加进来。

到这里 addWaiter 方法结束,节点已经被正确地添加到了尾部,我们回到 acquire() 方法,然后需要让这个节点进行排队等待锁,也就是 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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

先看到核心的for循环部分,这里的node还是我们上一个新创建的node节点,取到它的上一个节点,如果上一个节点是head节点,则调用子类的 tryAcquire() 方法尝试获取锁,如果获取成功,将此节点设置为头节点。这里的操作其实就是,将之前的头节点p,移除队列,此时没有任何地方指向这个p,这个p已经是垃圾对象了,然后将新的node节点设置为head,并且将这个ndoe的prev、next、thread都设置为null,只有这个节点的下一个节点的prev还指向着这个node。然后方法返回是否打断。再看另一种情况,如果p不是head,或者p是head但是未获取到锁,进入下一个if判断。

这里先讨论一下Node的几种状态。

状态说明
CANCELLED =  1表示这个节点已经被取消了,由于超时或者终端
SIGNAL =  -1表示这个节点处于等待唤醒状态,如果它的前一个节点释放了锁,该节点会被唤醒。这个节点的下一个节点需要被阻塞,当前节点如果释放了锁,需要唤醒下一个节点。
CONDITION = -2表示这个节点处于有条件的等待状态,位于等待队列中,需要等条件被唤醒后才会进入AbstractQueuedSynchronizer同步队列( condition.signal() )。
PROPAGETE = -3在Node的共享模式下,该节点可以无条件获取共享锁
0初始状态

继续接着上面的if判断,shouldParkAfterFailedAcquire(),参数为上一个节点,和当前这个节点。这个方法的返回值为是否需要阻塞当前线程,如果上一个节点是等待唤醒状态,直接返回true,表示当前节点需要被阻塞。如果prev节点是取消状态,则设置prev为prev.prev节点(即设置这个prev为它的上一个节点),并将当前节点的prev属性指向prev节点,整体而言,如果上一个节点是取消状态,则把这个节点出队,让后面的顶上来。用do-while操作,可以连续让多个取消状态的前驱节点出队,循环结束返回false即暂时不需要阻塞,等待下一次shouldParkAfterFailedAcquire被调用。如果是其他状态,将状态设置为SIGNAL状态,暂时也不需要阻塞,等待下一次shouldParkAfterFailedAcquire调用,要注意的是CONDITION这个状态并不会在这里出现,等待队列是由ConditionObject这个内部类维护的。

到这里shouldParkAfterFailedAcquire()结束,回到 acquireQueued() 方法中,通过shouldParkAfterFailedAcquire()我们知道了本次循环是否要阻塞当前线程。如果不需要阻塞,进入下一次for循环。如果需要阻塞,进入 parkAndCheckInterrupt() 方法将当前线程阻塞,返回并清除当前线程的中断标记,如果中断标记为true,则将 acquireQueued() 方法中的局部变量 boolean interrupted 设置为true。此时当前线程已经阻塞,需要其他线程来取消这个线程的阻塞状态,再开始下一次for循环。

概括一下这个for循环大体的操作,所有node节点线程都处于不断的for循环当中,每个node节点会根据前驱节点的状态来决定是否阻塞当前的线程(同时循环暂停)。当node节点发现自己的前驱节点为头节点时,才会尝试去获取一下锁,获取成功,将自己设置为头节点,中止for循环。

到这里,如果一个线程申请到了锁,也就是说跳出了acquireQueued() 方法,再回到 acquire() 方法,根据返回的中断标记来决定是否对当前线程执行中断方法。关于 interrupt()、interrupted()、wait()、sleep()以及中断标记相关的知识其实有蛮多可以讲的,这里不展开了。

2.2 独占锁的释放

释放的相关方法为 release() 、 tryRelease(),其中 tryRelease() 为模板方法,需要子类实现。

进入release(),首先调用子类的 tryRelease() ,如果释放成功,取到头节点,也就是正在释放锁的这个节点,通过 shouldParkAfterFailedAcquire() 方法我们可以知道,这个节点的状态正常情况应该是SIGNAL,也就是会调用到unparkSuccessor() 方法。我们进入unparkSuccessor() 方法,看到代码注释:如果头结点状态值小于0,将其设置为0,这里使用的是CAS机制,即使设置失败了,也无所谓,不影响后续操作。接下来是核心部分,这里要对下一个节点的线程进行唤醒(在获取的时候阻塞的),首先获取下一个节点,如果为null,或者节点状态为取消状态,进入for循环(担保机制),从尾节点开始往前开始遍历所有node,获取最靠近head节点并且状态不为取消状态的节点,如果存在这样的节点,把它的线程取消阻塞,使这个线程又开始了acquireQueued()中的for循环获取锁的操作(之前是被阻塞的)。

2.3 独占可中断获取锁

2.1介绍的是不可中断获取锁,这里是可中断获取锁,区别就在于在等待获取锁的过程中是否可以被中断。

具体方法为 acquireInterruptibly(),代码实现和2.1介绍的几乎一致,区别就在于,如果在节点循环获取过程中,如果parkAndCheckInterrupt() 方法返回了true,就直接回抛出中断异常,中止获取过程,同时进入finally中的逻辑。

cancelAcquire() 方法,首先获取到当前节点的前驱节点,为了方便描述,我们这个前驱节点暂时成为prevNode,如果prevNode状态为取消状态,将prevNode赋值为prevNode的上一个节点,并且将当前节点的上一个节点赋值为prevNode,这里是一个while循环,它会不断往前获取,直到某一个节点状态不为取消状态,其中跳过的节点(状态为取消状态)直接出队,这一幕似曾相识。while循环结束,将当前节点状态设置为取消状态,如果当前节点是尾节点,将当前节点的上一个节点设置为尾节点,简而言之就是如果当前节点是尾节点,就把当前节点出队。其他情况,方便描述,将当前node节点的前驱节点称为x,如果x不为head,并且满足 "x的状态为等待唤醒" 或 "状态小于0并且设置x的状态为等待唤醒成功" 两个条件之一,将x的next属性指向当前node节点的下一个节点;其他情况,将当前节点的下一个节点的线程设置为非阻塞,unparkSuccessor()方法实现。

2.4 独占可超时获取锁

实现和2.3几乎一样,在节点循环获取锁的过程中加入了时间的判断,如果超时,则抛出中断异常。

2.5 ReentrantLock

这个类中有3个内部类,分别是Sync抽象同步器、NonfairSync非公平锁同步器、FairSync公平锁同步器。

Sync是NonfairSync和FairSync的抽象父类。

NonfairSync部分源码如下:

        final void lock() {
            acquire(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();
            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;
        }

当我们获取独占锁的时候,调用lock方法,lock方法调用tryAcquire方法,如果state为0、队列为空、CAS设置state为1成功,则获取成功,返回true。如果state不为0,并且当前线程是锁的持有者,获取成功,这里实现了可重入锁。其他情况为获取失败,进入前面描述的同步队列进行排队。

释放同步锁代码如下:

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


       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;
        }

代码比较简单,通过设置state实现,先调用unlock,在调用父类的release方法,release方法会调用Sync中实现的模板方法tryRelease。

到这里有人可能会有疑问,公平锁、非公平锁,同样都是使用的AbstractQueuedSynchronizer的FIFO队列,队列中的Node节点都是按顺序获取锁,获取到之后,唤醒下一个节点,依次下去,这样的话,何来公平锁和非公平锁的?不都是公平锁吗?

答案就在NonfairSync和FairSync的lock方法中,跟踪代码,他们的lock方法大体相同,只有一行代码不同,FairSync中的if判断多了一个 !hasQueuedPredecessors()。

非公平锁的机制:如果新来了一个线程,试图访问一个同步资源,只需要确认当前没有其他线程持有这个同步状态,即可获取到。

公平锁的机制:既需要确认当前没有其他线程持有这个同步状态,而且要确认同步器的FIFO队列为空,或者队列不为空但是自己是队列中头结点指向的下一个节点。

这个区别很重要,因为线程在阻塞和非阻塞之间切换时需要比较长的时间,如果刚好线程A释放了资源,A会去唤醒下一个排着队的Node节点,当这个唤醒操作还没完成的时候,这时又来了一个线程B,线程B发现当前没人持有这个资源,于是自己就迅速拿到了这个资源,充分利用了线程A去唤醒B的这一段时间,这就是公平锁和非公平锁之间的差异,这里也体现了非公平锁性能较高的地方。

3. 后话

又是一堆枯燥的文字,有时候很矛盾,看源码到底有没有意义呢?新技术层出不穷,很多人都会很多新的技能,虽然可能没钻研过底层的实现,但是人家可能用的非常熟练,而且现在学东西都讲究速成,会用就行,何必看源码呢?这不是浪费时间吗?

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值