上一篇我们简单介绍了AQS的技术要点。本篇我们从ReentrantLock的角度分析AQS,帮助大家理解。首先我们看一下ReentrantLockSync的内部抽象类,这个是继承自AQS,重写了一些方法,我们在下面的源码中进行分析,继续往下看,记住这个Sync我们就知道这个锁可以实现公平锁和非公平锁,我们看下面是如何实现的(ExclareAnd1)setif(ExclareAnd1).currentThread());elseacquire(1);}protectedfinalbooleantryAcquire(intacquires){returnnonfairTryAcquire(acquires);}}/***Syncobjectforfairlocks*/staticfinalclassFairSyncextendsSync{privatestaticfinallongserialVersionUID=-30008978970904;6654(1);}}上面上面是非公平锁,下面是公平锁,默认是非公平锁。我们看非公平锁的实现是先通过CAS加锁。成功锁定锁后,当前线程被设置为活动锁持有者。线程/***Thecurrentownerofexclusivemodesynchronization.*/privatetransientThreadexclusiveOwnerThread;如果失败,就会执行acquire方法,OK,下面我们来看一下FairSync的lock方法的实现,公平锁不是像上面的非公平锁那样去判断,而是直接调用acquire方法这里大家也应该明白非公平锁和公平锁真正的区别,就是当非公平锁发生的时候,线程会比较多.尝试直接加锁一次,其余操作同理。OK,我们进入acquire方法,看看tryAcquire方法。可以看出这只是AQS的简单实现。获取锁的具体实现方式由各自的公平锁决定,非公平锁分别实现(以ReentrantLock为例)。如果该方法返回True,说明当前线程已经成功获取到锁,后面不需要再执行;如果获取失败,需要加入等待队列。下面将详细解释线程是如何在何时以及如何被添加到等待队列中的。OK,知道了这些,我们就来看看ReentrantLock是如何实现tryAcquire方法的老规矩的。首先我们来看一下非公平锁的具体实现。大家应该更容易理解代码。第一步判断state==0,这个0表示共享资源空闲,所以这里先尝试抢锁。如果此时等待队列中有等待线程,则为等待线程中的第二个节点,新加入的线程为什么是第二个抢到这个锁的,因为第一个头节点一直存储着线程节点Node占用锁?接下来判断当前持有锁的线程是否与当前线程相同。如果相同,则state+1,这里是ReentrantLock支持可重入性的关键。解锁的时候,也是通过减去状态计数来抢锁或者重新入锁,会返回true,返回true,加锁的方法直接加锁,如果锁被加锁,而占用锁的线程不是发现是当前线程,就会返回false,继续执行上面的tryAcquire方法,这是一个非公平锁。接下来我们看一下这个公平锁的tryAcquire方法。这也是先判断状态。是否为0,这个==0之后的处理逻辑很清楚,直接通过hasQueuedPredecessors方法判断队列中是否有等待节点,如果没有等待节点,直接通过CAS判断,然后把当前的If线程设置为活跃线程,如果有等待节点,则跳过CAS的判断,然后判断当前线程和持有锁的线程是否是同一个线程。如果是同一个线程,还是会算+1。如果重入不满足,则返回false。这时tryAcquire方法返回false。这时候我们把视角拉回acquire方法并返回false,然后执行addWaiter方法和acquireQueued方法。这段代码会先创建一个与当前线程绑定的Node节点,Node是一个双向链表。此时waitingpair中的tail指针为空,直接调用enq(node)方法将当前线程加入waitingqueue的尾部:第一个循环tail指针为空,进入if逻辑上,使用CAS操作设置head指针,将head指针设置为一个新创建的Node节点。此时AQS中的数据:执行完成后,head、tail、t都指向第一个Node元素。然后执行第二个循环,进入else逻辑。这个时候已经有头节点了。这里需要操作的是将线程2对应的Node节点挂在头节点后面。此时队列中有两个Node节点:执行完addWaiter()方法后,会返回当前线程创建的节点信息。继续执行acquireQueued(addWaiter(Node.EXCLUSIVE),arg)逻辑。此时传入的参数是线程2对应的Node节点信息。acquireQueued()这个方法会先判断当前传入的Node对应的前一个节点是否为head,如果是则尝试加锁。如果加锁成功,则将当前节点设置为头节点,然后将之前的头节点空出来,以便后面进行垃圾回收。如果锁失败或者Node的前端节点不是头节点,会通过shouldParkAfterFailedAcquire方法将头节点的waitStatus改为SIGNAL=-1,最后执行parkAndChecknIterrupt方法调用LockSupport.park()挂起当前线程。我们发现不了的是AQS的设计内部,包括ReentrantLock的设计内部。很多地方会尝试使用CAS加锁,因为在高速运行下,一个线程可能几行代码就把锁用完了,这样才能最高效地使用资源。parkAndCheckInterrupt主要用于挂起当前线程阻塞调用栈,返回当前线程的中断状态。我这里给大家看一下流程图。图片来自网络,我觉得还不错。从上图可以看出,跳出当前循环的条件是“前一个节点为头节点,当前线程获取锁成功”。为了防止死循环造成CPU资源的浪费,我们会通过判断前端节点的状态来决定是否挂起当前线程。具体挂起流程如下图所示(shouldParkAfterFailedAcquire流程):在最后finally中acquireQueued中,如果失败,则执行cancelAcquire获取当前节点的前驱节点。如果前驱节点状态为CANCELLED,则一路向前遍历,找到第一个waitStatus<=0的节点,将找到的Pred节点与当前Node关联起来。当前节点设置为取消。但是为什么所有的变化都是对Next指针进行操作,而不对Prev指针进行操作呢?Prev指针在什么情况下会被操作?在执行cancelAcquire时,当前节点的前一个节点可能已经从队列中移除(Try代码块中的shouldParkAfterFailedAcquire方法已经执行完毕),如果此时修改Prev指针,可能会导致Prev指向另一个已从队列中移除的Node,因此更改Prev指针是不安全的。在shouldParkAfterFailedAcquire方法中,会执行下面的代码,其实就是在处理Prev指针。shouldParkAfterFailedAcquire仅在获取锁失败时执行。进入该方法后,表示共享资源已经获取,当前节点之前的节点不会发生变化。因此,此时更改Prev指针比较安全。做{node.prev=pred=pred.prev;}while(pred.waitStatus>0);解锁接下来分析一下解锁的基本流程。由于ReentrantLock在解锁的时候不区分公平锁和非公平锁,我们直接看解锁的源码:点击release后,发现在公平锁和非公平锁的父类中实现还是在AQS框架中ReentrantLockSync定义了可重入锁的锁释放机制。该方法先减少一次重入次数,然后判断当前线程是否为持有锁的线程。如果不是,则直接抛出异常然后判断c==0,等于0表示当前资源空闲,然后可以将当前独占资源线程设置为null,如果是则更新状态不等于0,这一步释放排他锁的操作会被过滤掉,即普通可重入锁减少1次重入次数,就像重入3次加锁一样,执行完这个后,只变成2倍,但线程仍然持有资源。总结我们首先从非公平锁和公平锁的角度分析了加锁过程,了解到非公平锁只是比公平锁多了一个机会先加锁,但是抢不到锁还是会执行和公平锁一样的逻辑。中间我们分析了公平锁和非公平锁的优缺点。这是面试中的一个热点,我们也会发现代码中很多地方都会尝试使用CAS方式来抢占锁。我们知道CPU运行速度非常快,可以保证资源的释放能够在第一时间被等待队列中的线程抢到。最后我们分析了释放锁的过程。这个释放锁没有公平和不公平的区别,只是处理了重入锁,也就是上图最后一张图中的==0操作,因为我们上面分析的重入原因也是这个状态积累的,所以这里你只需要减一,然后判断它是否为0。当为0时表示此时资源空闲,这个状态是volatile的,保证了可见性。这只是一般的分析。其实还有很多细节没有分析到位。只能说AQS的设计非常精致。
