深入浅出 JUC 之 AQS 与锁

142 阅读23分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

小伙伴们,大家好,这里是追風者。在面试时,面试官一定问过一个问题:你了解 Java 中的锁吗?都有什么类型的锁?独占锁、共享锁、公平锁、非公平锁都了解多少?

我在第一次听到这个问题的时候,当场发懵(确实太菜了),那么今天就来聊聊锁的体系。

locks.png

可以看到,在 rt.jar 中,java.util.locks 包下,就是所有的锁相关的类了。等会我们会对其中较为较为重要的类进行解析。

这里首先要说的是 AQS,它是一个抽象类,很多锁实现锁功能的部分都继承了 AQS。

下面拿 ReentrantLock 举个例子(别问为啥,问就是简单,你看看其他的锁,内部类一串一串的🤢),ReentrantLock 实现获取/释放锁的功能在 Sync 的内部类,而 FairSync 公平锁和 NoFairSync 非公平锁都是继承了 Sync 的。

下面的所有代码分析都需要通过你得用编译器一行一行跟着走哦,带你了解整个锁的体系,下面出发吧。

AQS

AQS 全名为 AbstractQueuedSynchronizer,它有共享模式和独占模式两种模式。

我们先来分析 AQS 的结构,首先 AQS 有两个内部类结构,Node 和 ConditionObject,Node 就是在队列中的节点,ConditionObject 是用来管理条件队列使用的,后面再详解。

AQS 有两种队列,一为阻塞队列,二为条件队列。阻塞队列就是实现锁时,保存线程阻塞的队列。条件队列时实现生产者-消费者模型时,进行阻塞的队列,条件队列中的节点最后还需要迁移到阻塞队列中。两队列的节点都是 Node。

这里先说 Node 的结构。

 Node 节点有多个属性:

 - state 0表示无线程竞争到锁,被 volatile 修饰,线程安全。

 - exclusiveOwnerThread 保存获取到锁的线程的引用,从 AbstractOwnableSynchronizer 继承而来。

 - prev 前驱

 - next 后继

 - thread Node 节点包含的线程

 - nextWaiter 条件队列的后继(条件队列为单项链表),有时也会将其存储 Node 的身份,如该节点时共享模式还是独占模式。

 - waitStatus 节点状态,根据该数值可以知道节点处于某种状态,以及后续节点的操作方式。

waitStatus

waitStatus 分别有以下取值:

  1. CANCELLED 值为1,表示线程在阻塞队列中因等待超时或者被中断而取消了,即该节点可以被阻塞队列剔除了。

  2. SIGNAL 值为-1,后继节点处于阻塞状态,当前节点释放锁后,需要 park 唤醒。

  3. CONDITION 值为-2,该节点在条件队列 Condtion 中,当其他节点对 Condtion 调用 signal(),该节点从条件队列移动到阻塞队列中。

  4. PROPAGATE 值为-3,表示下一次共享式同步状态获取被无条件传播。

  5. 0,默认即0。

好,柿子先挑软的捏,解析 AQS 就从 ReentrantLock 开始,因为 AQS 是采用模板方法的设计模式,通过继承 AQS 而重写部分方法来进行总体流程不变的,只不过是某些方法有些差异。

AQS 的独占模式

获取独占锁解析

秉承着知其然知其所以然的原则,首先来阐述一下获取锁的机制,然后在代码中分析是否如此。

首先要明白 Node 的 waitStatus 的状态意义。其次,在获取独占锁时,注意使用的是 Node.SIGNAL 状态,它表示节点的后继需要我来 unPark() 来激活。

中心思想就两点:

  1. 尝试获取锁,即 CAS 修改 state,如果获取锁成功就将 exclusiveOwnerThread 设置为当前线程。

  2. 获取不到锁,就将线程封装为 Node 加入到阻塞队列中,并且要将前驱的 waitStatus 设置为 Node.SIGNAL,由前驱节点负责唤醒当前线程。

  3. 获取不到锁就维护阻塞队列。

获取独占锁的调用链路如下:ReentrantLock.lock() -> Sync.lock() -> ReentrantLock.NonfairSync.lock() (此时是公平锁和非公平锁都行,随便选了个非公平锁)->  AbstractQueuedSynchronaizer.acquire() -> ReentrantLock.tryAcquire() -> nonfairTryAcquire() -> AbstractQueuedSynchronaizer.addWaiter() -> AbstractQueuedSynchronaizer.acquireQueued() -> shouldParkAfterFailedAcquire() -> parkAndCheckInterrupt() -> cancelAcquire() -> selfInterrupt();

调用锁自己实现的公平或非公平锁,即 ReentrantLock.NonfairSync.lock()。

该方法首先对 state 进行 CAS 操作,判断其是否为0,为0就替换为1,并将获取到锁的线程替换为当前线程。

如果有线程获取到锁了,就调用 acquire(1)。

acquire() 是对争抢锁的封装,首先尝试争抢锁,如果争抢不到锁就加入阻塞队列。

tryAcquire() 就是尝试争抢锁。它有调用了 nonfairTryAcquire(),顾名思义 非公平争抢锁。公平锁与非公平锁也是在此处实现不同而导致的锁不同

首先获取到当前线程,而后对 state 进行判断,如果等于0,就对 state 属性进行 CAS 操作,从0替换为目标值,替换成功后,将 exclusiveOwnerThread 设置为当前线程即可,返回 true。

若 state 不为0,就判断当前线程 与 exclusiveOwnerThread 是否相等,如果相等,就将 state + 参数值,表示重入锁重入了几次,返回 true。如果都不符合就返回 false 了。

当 tryAcquire(arg) 为 false 时,就调用了 addWaiter(Node.EXCLUSIVE),此方法将线程封装为 Node 加入到阻塞队列中。此时将线程包装为 Node,并为其 nextWaiter 赋值为独占模式的 Node。然后获取到双向队列的尾节点,如果尾节点不为空,就将 当前线程的 node 入队尾,如果为空,调用 enq(node),最后将 node 返回。注意,此方法返回的就是 node,虽然 enq() 返回了 node 的前驱,但是没有接收。

enq() 方法是通过自旋方式,来构建出阻塞队列,也可以将节点入队。当尾节点不存在时(即队列不存在节点),CAS 一个空的 Node 对象给队列首部并将队列的首尾相连。此时就有了一个首尾相连的空队列了,由于是自旋,再次进入循环后,节点就会插入到队尾了(先把 node 的前驱设置为尾节点的指针,而后 CAS 替换尾节点指向的节点,CAS 成功后,尾节点就是 node 了,但是此时的 t 还是指向上一个节点,最后将原尾节点的后继指向 node,插入就完毕了),将 node 的前驱返回,也就是将原尾节点返回。

acquireQueued(node, age),此时的参数前一个为 当前线程在阻塞队列的前驱节点,一个为获取锁的数量。这个函数的作用是:将 node 设置为头节点,使其线程执行;如果争抢不到锁就阻塞。进入函数,首先声明一个标志位 failed 为 true,只有当前函数出现异常的时候,它才为 true。自旋。获取到 node 的前驱节点 p,判断 p 是否为队列的头节点,并调用函数 tryAcquire() 尝试获取锁。若上面判断都通过,就将 node 设置为头节点,将 p 的后继置空,此时 p 就没有外部引用它了,就是垃圾,在下次垃圾回收时会被回收(垃圾回收机制以后会有专门的文章)。将 failed 设置为 true,将自旋循环外的 interrupted 返回。当判断没通过时,第二个 if()。先看第一个函数。

shouldParkAfterFailedAcquire(p, node),p 为 node 的前驱节点。此方法的目的:将 node 的前驱的 waitStatus 设置为 SIGNAL 表示需要其唤醒 node(因为此时当先线程可能没有获取到锁,等会就会阻塞掉了),若 p 的 waitStatus 为 SIGNAL 返回 true。pred 的 waitStatus 记为 ws。当 ws 为 SIGNAL 时,直接返回 true。如果 ws 大于0,不断向节点前驱查询,直到 ws 不大于0,将其后继设为 node即可。如果 ws 是小于0的,那么直接通过 CAS 将其 ws 设置为 SIGNAL 即可。最后返回 false。

判断通过就执行 parkAndCheckInterrupt(),此方法就是将线程阻塞掉。暂时线程就停在这里了(下面内容的建议看完释放锁后继续看)。

只有等待前驱节点释放锁后激活。激活后就直接返回线程的中断状态。如果线程被中断了,if() 条件通过,将 interrupted 置 true,重新进入循环,节点获取到锁,返回了 interrupted。回到 acquire(int arg) 方法,执行 selfInterrupt(),即将当前线程中断(但此流程的方法都是不响应中断的,暂时无用)。

如果线程未被中断,那么就执行资源区代码了。

如果线程在 acquireQueued() 方法自旋时出现异常,最后就会走 finally 的 cancelAcquire(node) 方法。

cancelAcquire(node) 方法的目的在于将 node 剔除队列,不影响理解,就不讲了,大家也能看懂。

释放独占锁解析

释放独占锁的调用流程:ReentrantLock.unlock() -> AbstractQueuedSynchronizer.release() -> ReentrantLock.tryRelease() -> AbstractQueuedSynchronizer.unparkSuccessor();

首先分析 tryRelease(arg) 方法,方法的目的就是尝试释放锁。首先,将 state 与 arg 做差值 c。如果当前线程与 exclusiveOwnerThread 不相等的话,就出异常了,因为释放锁必须获取到锁。如果 c 为0才表示锁彻底释放(因为是可重入锁)。如果 c 是0,表示锁彻底释放。最后返回结果。返回到 release() 方法。

release() 方法目的是:当前线程释放锁,将阻塞队列的头节点的后继节点激活。锁彻底释放后,就可以将 h 的后继进行激活了。执行 unparkSuccessor(h) 方法。

unparkSuccessor() 方法是将节点的后继激活。首先将当前节点的 waitStatus CAS 设置为0。获取到 node 的后继 waitStatus 不为 CANCELLED 的节点(CANCELLED 表示节点被取消了),然后 unpark 激活线程即可。最后根据结果返回即可。

条件队列

除了阻塞队列,AQS 还维护了一个条件队列,该队列在一些线程安全的容器中被使用。条件队列中的元素最后还有迁移到阻塞队列中。

条件队列的缔造者为 Condition 接口,它定义了等待和唤醒的方法供人实现。AQS 的 ConditionObject 就是实现了该接口。主要分析 await() 方法的调用链和 signal() 的调用链。

条件队列主要为 ConditionObject 来服务,通过 ConditionObject 的 await() 和 signal() 实现等待/通知模型。

await()

await() 方法是将线程阻塞进入条件队列。首先看第一个被调方法 addConditionWaiter(),该方法就是将线程构造为 Node 后入队。先获取到条件队列的队尾元素 t,然后主要判断 t 的 waitStatus 是否为 CONDITION(即表示节点在条件队列中)。如果不符合就调用 unlinkCancelledWaiters() 方法,该方法的目的是剔除条件队列中,所有状态不是 CONDITION 的节点。

unlinkCancelledWaiters() 方法中,从首节点到尾节点遍历的方式,不断判断节点状态是否为 CONDITION,如果不是就将节点的条件后继节点置空,其前驱的后继会指向当前节点的后继,即节点已经不在条件队列中了。

从 unlinkCancelledWaiters() 出来后,此时的 t 才是货真价实的条件队列尾节点。将当前线程构建为 node 节点,入队并返回。

此时回到了 await() 方法中,node 即当前线程的节点对象。调用 fullyRelease(Node node) 方法来完全释放独占锁。

fullyRelease(Node node),首先标志位 failed 为 true,只有方法出现异常时,finally 中的 if 语句才会执行,即将当前节点的状态设置为取消。获取到 state 数值,调用 release(savedState) 方法,如果成功就返回释放锁前的锁数量,失败就抛出异常,被 finally 处理。release(savedState) 就不细说了,上面讲独占锁的释放主要就讲了这个函数,它就是将阻塞队列头节点的后继节点 unpark 唤醒。

isOnSyncQueue(node) 方法,该方法时判断节点是否在阻塞队列中。对节点 waitStatus 状态进行判断,如果在条件队列就返回 false,如果节点后继不为空,就返回 true(因为条件队列中,通过 nextWaiter 来指向下一个节点)。如果以上的判断都不通过,就调用 findNodeFromTail(node) 方法并返回。该方法就是将同步队列从尾节点开始,不断前移判断 node 是否相同,如果有相同的就要返回 true,如果遍历到最后 t 等于空了,就表示 node 不在阻塞队列中,返回 false。

如果节点不在阻塞队列中,那么当前线程就要被 park 阻塞了(以下建议阅读 signal() 后再继续阅读)。

线程被唤醒后,调用 checkInterruptWhileWaiting(node) 方法,判断线程在挂起阶段是否被中断过,如果被中断过,调用 transferAfterCancelledWait(node)。该方法能够判断出线程在 signal 前中断还是 signal 后中断。如果 node 的 waitStatus 是 CONDITION,就 CAS 操作替换为0,然后节点进入阻塞队列,最后返回 true,此时就是 signal 前中断,因为 signal 会将 node 状态置为0。如果 CAS 操作失败,循环判断是否在阻塞队列中,不存在就将线程让步,重新争抢锁(Thread.yield())。如果在阻塞队列中,直接返回 false,此时表示线程在 signal 后中断的。

调用 acquireQueued(node, savedState) 方法,该方法是将 node 设置为头节点,使其线程执行;如果争抢不到锁就阻塞。只有当线程被中断过,并且 interruptMode 不是 THROW_IE 的时候,将中断状态恢复,用于重新中断。

如果节点的条件队列后继不为空,调用 unlinkCancelledWaiters() 方法(signal 前被中断,因为 signal 会将 nextWaiter 置空)。

unlinkCancelledWaiters() 该方法的目的是剔除条件队列中,所有状态不是 CONDITION 的节点。前面讲过。

如果 interruptMode 不为 0时,表示被中断过,复位进行中断操作。reportInterruptAfterWait(int interruptMode) 方法根据传入到方法,选择进行复位中断还是抛出异常。

signal()

signal() 方法就是唤醒条件队列中的节点。首先调用 isHeldExclusively() 方法。

isHeldExclusively() 方法是判断当前线程是否为获取到锁的线程,不是就抛出异常。获取到条件队列的头节点 first,调用 doSignal(first) 方法。

doSignal(first) 方法将头节点移除,并将后继节点唤醒做头节点。首先将条件队列的头节点设置为 first 的后继节点,如果是空的,就把尾节点直接置空。将 first 的后继节点置空。调用 transferForSignal(first) 方法。

transferForSignal(first) 方法是将 first 迁移到阻塞队列,并将头节点的线程唤醒。首先通过 CAS 尝试修改节点的 waitStatus 为0,修改不成功就返回 false。将 node 迁移到阻塞队列中,并获取其前驱节点 p。如果 p 的 waitStatus 是大于0的(表示节点线程被取消),或者 CAS 将 p 的 waitStatus 替换为 SIGNAL 失败的话,就将 node 的线程唤醒,返回 true。

AQS 的共享模式

AQS 的共享锁通过 CountDownLatch 来分析。

CountDownLatch 就是使用的 AQS 的共享模式实现的。

共享模式就是将多个线程能够公有一个锁,即多个线程组成一个锁,只有多个线程都释放锁后,锁才算被释放了。该模式不是使用锁来限定临界区了,而是采用了 AQS 的阻塞队列来使多个线程进行集体等待操作的模式。

CountDownLatch 只声明了有参构造方法,其构造方法的参数是为 AQS 的 state 进行赋值的。思想就是 state 个线程来解锁,就放行。

总体来说,构造时将 state 传入,await() 方法不断判断 state 是否为0,不为0就进入阻塞队列等待。而 countDown() 方法是对 state 进行减一修改,直至 state 为0时,放行。

await()

await() 的方法调用链如下:CountDownLatch.await() -> CountDownLatch.Sync.acquireSharedInterruptibly() -> CountDownLatch.Sync.tryAcquireShared() -> AbstractQueuedSynchronizer.doAcquireSharedInterruptibly();

直接从 acquireSharedInterruptibly(int arg) 方法开始,该方法是为了判断是否有 state 数量个线程进行等待了,如果数目不足,就将当前线程构建为 Node 塞入阻塞队列,并且该方法响应中断。首先判断线程的中断状态,如果被中断就直接抛出异常跳出方法了。进入 tryAcquireShared(arg) 方法尝试获取共享锁。tryAcquireShared() 方法就判断了一下 state 是否为0,即共享锁预设的等待线程数量,是则返回1,反之返回-1。

判断完 state 的数量后,通过判定就进入 doAcquireSharedInterruptibly(arg) 方法,该犯方法就要将当前线程塞入阻塞队列了。

首先调用 addWaiter() 方法,将当前线程构建为 Node 节点,并指明其为 SHARED 共享节点。获取到阻塞队列的队尾,判断该队尾是否为空,如果为空或者 CAS 替换阻塞队列队尾为当前线程的 node 失败的话,就调用 enq(node) 来自旋入队。最后返回当前线程的 node 节点。返回到了 doAcquireSharedInterruptibly() 方法,设置 failed 标志位并将其置 true。此时进行自旋操作。获取 node 的前驱节点,如果该节点为头节点,就调用 tryAcquireShared(arg) 方法来尝试获取共享锁,如果大于0,就表示 state 为0了。共享锁已经释放了,当前线程可以被执行。调用 setHeadAndPropagate() 方法。

setHeadAndPropagate(node, r) 方法是将 node 设置为阻塞队列的头节点即指明当前线程可以执行,将 node 设置为阻塞队列头节点后,将 node 找一个合适的后继。获取到 node 的后继 s 后,s.isShared() 是判断 s 是否为共享节点。调用 doReleaseShared() 方法。

doReleaseShared() 方法是将头节点的 waitStatus CAS 为0,如果 CAS 操作成功,就将调用 unparkSuccessor() 方法,将头节点的后继线程激活。如果头节点的 waitStatus 状态为0,并且 CAS 其状态为 PROPAGATE 操作失败后,就跳过此次循环。

此时又回到了 doAcquireSharedInterruptibly() 方法,将 p 后继置空,为垃圾,等待回收即可。标志位置  false ,方法返回。

shouldParkAfterFailedAcquire() 方法就是找到 node 的合适的前驱(waitStatus 为 SIGNAL 的),该方法在前面分析过。而后调用 parkAndCheckInterrupt() 来 park 阻塞线程。finally 的前面也分析过,就不重复了。

countDown()

countDown() 的方法调用链:countDown() -> AQS.releaseShared() -> tryReleaseShared() -> doReleaseShared() -> unparkSuccessor();

首先来解析 tryReleaseShared(arg) 方法。该方法通过自旋方式来将 state 减一操作。最后判断新的 state 是否为0并返回。

当 state 为0时,进入 doReleaseShared() 方法。该方法进行共享锁的释放。在上面的 await() 方法中讲过了。可能有人会有疑问,await() 方法是阻塞线程用的,为什么还会执行激活线程、释放共享锁的操作?起始当 await() 进入到 doReleaseShared() 方法的前提就是 state 必须为0,那么共享锁也就释放掉了。

Lock

从文章开头的 JUC locks 包截图中,locks 包下面,除了 AQS 体系,也只有三种锁了,那就是 ReentrantLock 可重入锁、ReentrantReadWriterLock 可重入读写锁和 StampedLock 信号量锁。

既然聊到了锁,就要说一下乐观锁、悲观锁、公平锁和非公平锁。

乐观锁:乐观锁就是认为操作时不会有并发问题,因此就不上锁。典型的例子就是 CAS 操作,通过比较交换的方式进行数据更改。乐观锁的精髓就是不阻塞

悲观锁:悲观锁总是假定最坏的情景,每次进行数据操作时都会加锁,一旦加锁,就只能有一个线程执行。JUC locks 包下的锁,都是悲观锁。悲观锁的精髓就是加锁

公平锁:公平锁就是每个线程,被唤醒后,按申请顺序获取到锁,不会被后来的线程抢占。

非公平锁:每个线程被唤醒后,不一定哪个线程会获取到锁。

下面来分析一下各个锁。

ReentrantLock

ReentrantLock 是可重入独占锁,它实现了 Lock 接口。其内部类 Sync 实现了 AQS 抽象类,实现了获取锁、释放锁等方法,内部类 NonfairSync 和 FairSync 继承 Sync,进行了lock() 和 tryAcquire() 公平锁和非公平锁的实现。

ReentrantLock.png

通过前面 AQS 的解析我们能了解到,独占锁的获取就是对 state 赋值,然后将 exclusiveOwnerThread 设置为当前线程,如果获取不到就进入阻塞队列。

ReentrantLock 无参构造函数为非公平锁,有参构造函数可选为公平锁。因为相比公平锁,非公平锁性能更高。

我们从源码入手来看 ReentrantLock 公平锁与非公平锁的实现区别。


        // 非公平锁

        final boolean nonfairTryAcquire(int acquires) {

            final Thread current = Thread.currentThread();

            int c = getState();

            if (c == 0) {

                if (compareAndSetState(0, acquires)) {

                    setExclusiveOwnerThread(current);

                    return true;

                }

            }

            // 若当前线程与获取到锁的线程是同一线程时

            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) {

                // 与非公平锁不同的就是多了方法 hasQueuedPredecessors() 判断

                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;

        }

hasQueuedPredecessors() 方法,h != t 表示队列不为空。((s = h.next) == null || s.thread != Thread.currentThread()) 如果按部就班来,此语句一定会为 false,就可以获取锁了,那么什么时候该语句为 true 呢?那么就是阻塞队列中只有一个头节点了。

hasQueuedPredecessors() 方法返回 false 才能获取到锁,又因为获取独占锁时,头节点在获取到锁后才更替,可以得出结论:阻塞队列为空时、阻塞队列中只有头节点时可以获取到锁,其他情况无法获取到锁,即阻塞队列中仍有排队情况时,就无法获取到锁,公平。

释放锁操作,上面 AQS 举例了,就不赘述了。

ReentrantLock 主要内容就是公平锁和非公平锁的获取了,剩下的方法都是获取一些状态,都能看懂。

ReentrantReadWriteLock

ReentrantReadWriteLock 可重入读写锁,读读共享,读写互斥,写写互斥。

ReentrantReadWriteLock 中有多个内部类,Sync 是继承了 AQS, NonfairSync 和 FairSync 分别实现了非公平和公平的 读写锁阻塞方式。

ReentrantReadWriteLock.png

获取读锁。

内部类 ReadLock 中的 lock() 方法,它获取的是共享锁。方法调用链为: ReentrantReadWriterLock.ReadLock.lock() -> AQS.acquireShared() -> ReentrantReadWriterLock.Sync.tryAcquireShared() -> AQS.doAcquireShared();

直接进入 ReentrantReadWriterLock.Sync.tryAcquireShared() 方法。该方法返回 -1 就表示获取读锁失败了,返回 1 表示获取读锁成功。

提前声明,在此处,AQS 的 state 不仅仅代表锁的重入次数了,因为有读锁和写锁。在此处, state 的高 16 位表示了获取读锁的线程个数,低 16 位表示写锁的重入次数(因为读锁是共享锁,写锁是排他锁,即独占锁)。

exclusiveCount(c) 方法留下了 c 的低 16 位,即写锁重入的次数。如果存在写锁并且获取到写锁的不是当前线程,获取读锁就失败了。后面会有写锁的锁降级,所以线程获取到写锁后,仍然可以获取读锁,最后释放写锁即可

sharedCount(c) c 无符号右移16位,留下高16位,即 r 为获取到的读锁数量。

readerShouldBlock() 此处先跑通流程,流程理解后再分析公平和非公平问题,此处使用的是非公平锁。

apparentlyFirstQueuedIsExclusive() 方法是判断头节点的后继节点是否为独占模式,是独占模式就返回 true(此处的独占模式就是写锁)。

当 readerShouldBlock() 返回 false 时,表示阻塞队列中有效头节点为共享节点,即读线程节点(无视真正的头节点,因为其表示正在运行的线程节点)。

此处的 CAS 操作,就是将 c 的高 16 位加一,表示获取读锁的数量也加一。

此时进入到 if 判定区内,就表示获取锁已经成功了,修改一些数据状态就行了,最后都会返回 1。首先判断 r == 0,表示当前无读线程,把 firstReader 设置为当前线程(firstReader 表示第一个获取读锁的线程)。并将 firstReaderHoldCount 赋值为1(firstReaderHoldCount 为 锁重入次数)。第二个判定首个获取读锁的线程是否为当前线程,如果时当前线程,直接将 firstReaderHoldCount 自增。最后两个判定都不通过,获取到缓存的 HoldCounter,HoldCounter 中存储了重入读锁次数和获取读锁的线程id。获取到数据,并将 count 自增。

如果上面的获取锁判定失败,执行 fullTryAcquireShared(current) 方法。该方法通过自旋方式,对上面的三个判定进行重新判定,从而尝试获取锁。

自旋中的第一个判定,exclusiveCount(c) != 0 表示阻塞队列有效头节点为独占模式,如果此时获取到锁的不是当前下线程,仍然无法获取到锁,返回 -1。第二个判定,阻塞队列的有效头节点为独占模式。此时判断当前线程是否为 firstReader,此处判断时为了防止多次获取到读锁,导致缓存中数据错误。else 语句中,首先获取到缓存中的 HoldCounter,如果 rh 不是当前线程的,就从 ThreadLoacl 中获取,如果 count 是0,表示刚刚初始化的,就将 ThreadLoacl 中的缓存移除掉,返回 -1。

如果 state 的高16位与 MAX_COUNT 相等,表示线程无法获取读锁,超出最大读锁数量。

最后 CAS 操作将 state 的高 16 位加一,然后将锁的数量进行记录,最后返回 1。

获取读锁失败后,执行 doAcquireShared(arg) 方法。该方法主要是将线程加入阻塞队列,前文解析过,就不细说了。

释放读锁

由于 ReentrantReadWriterLock 继承了 AQS,调用的是 AQS 的模板方法 releaseShared(),所以只分析一下 ReentrantReadWriterLock.tryReleaseShared() 方法。该方法尝试释放锁。首先获取到当前线程,判定 首个获取读锁的线程是否为当前线程,如果是当前线程,还有判定 firstReaderHoldCount 是否为1,如果为1,就置空,否则自减。如果 firstReader 不是当前线程,就要查看 ThreadLocal 中的缓存了,如果 count 小于等于1,就将缓存移除,否则将数值自减。

最后,通过自旋操作更新读锁的 state,将其高 16 位减一,最后返回 state == 0 的值判定是否完全释放锁。

获取写锁

写锁直接调用的 AQS.acquire() 方法,模板方法模式,只分析 ReentrantReadWriterLock.Sync.tryAcquire() 方法。方法体第三行,exclusiveCount(c) 获取到 state 低 16 位,获取到写锁的重入数。首先判断,如果写锁重入数 w 为 0 或者当前线程并不是获取到写锁的线程,无法获取到锁,返回 false(此时读锁数量不为0,或者获取到写锁的不是当前线程)。锁的数量超出限制会抛出异常。否则就能够获取到锁,将 state 加一,返回 true。

如果 state 为0,那么执行 writerShouldBlock() 方法,进入非公平锁中,直接返回 false,直接对 state 进行 CAS 操作。如果失败就返回 false,获取锁失败。获取锁成功的话就将当前线程设置为获取锁的线程,并返回 true。

释放写锁

老规矩,直接解析 ReentrantReadWriterLock.Sync.tryRelease() 方法。判断获取锁的线程就不说了。将 state 减去参数表示释放了几层写锁,获取 state 低 16 位,判断是否为0,表示锁是否全部释放。如果锁全部释放了,就将 exclusiveOwnerThread 置空,然后将计算后的 state 设置回去,返回 free 表示释放锁是否成功。

写锁的公平/非公平锁

通过 FairSync 和 NonfairSync 的写锁阻塞方式,可以判断出两者的不同。公平锁会判断阻塞队列中是否还有阻塞的线程节点来判断是否尝试获取锁,而非公平锁会直接尝试获取锁。

读锁的公平/非公平锁

通过 FairSync 和 NonfairSync 的读锁阻塞方式,可以判断出两者的不同。非公平锁会根据有效头节点的模式来进行判断是否争抢锁,而公平锁会判断阻塞队列中是否还有阻塞的线程节点来判断是否尝试获取锁。

如果文章有任何错误欢迎各位斧正,编程心得就是需要不断的交流才会拓宽视野,感谢各位。