哈喽大家好,我是阿Q。,最近是上班忙项目,下班带娃,忙的不可开交,连摸鱼的时间都没有了。今天趁假期用图解的方式从源码角度给大家说一下ReentrantLock加锁解锁的全过程。系好安全带,发车了。,在聊它的源码之前,我们先来做个简单的使用说明。当我在IDEA中创建了一个简单的Demo之后,它会给出以下提示,
,在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。,1、如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。,2、如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。,3、在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。,java.concurrent.LockShouldWithTryFinallyRule.rule.desc,还举了两个例子,正确案例如下:,错误案例如下:,上边的案例中加锁调用的是lock()方法,解锁用的是unlock()方法,而通过查看源码发现它们都是调用的内部静态抽象类Sync的相关方法。,abstract static class Sync extends AbstractQueuedSynchronizer,Sync 是通过继承AbstractQueuedSynchronizer来实现的,没错,AbstractQueuedSynchronizer就是AQS的全称。AQS内部维护着一个FIFO的双向队列(CLH),ReentrantLock也是基于它来实现的,先来张图感受下。,
,对于里边的waitStatus属性,我们需要做个解释:(非常重要),今天我们先简单了解下AQS的构造以帮助大家更好的理解ReentrantLock,至于深层次的东西先不做展开!,对AQS的结构有了基本了解之后,我们正式进入主题——加锁。从源码中可以看出锁被分为公平锁和非公平锁。,
,初步查看代码发现非公平锁似乎包含公平锁的逻辑,所以我们就从“非公平锁”开始。,compareAndSetState():底层调用的是unsafe的compareAndSwapInt,该方法是原子操作;,假设有两个线程(t1、t2)在竞争锁资源,线程1获取锁资源之后,执行setExclusiveOwnerThread操作,设置属性值为当前线程t1,
,此时,当t2想要获取锁资源,调用lock()方法之后,执行compareAndSetState(0, 1)返回false,会走else执行acquire()方法。,accquire()中涉及的方法比较多,我们将进行拆解,一个一个来分析,顺序:tryAcquire() -> addWaiter() -> acquireQueued(),因为线程1已经获取到了锁,此时state为1,所以不走nonfairTryAcquire()的if。又因为当前是线程2,不是占有当前锁的线程1,所以也不会走else if,即tryAcquire()方法返回false。,走到本方法中,代表获取锁资源失败。addWaiter()将没有获取到锁资源的线程甩到队列的尾部。,当tail不为空,即队列中有数据时,我们来图解一下pred!=null代码块中的代码。初始化状态如下,pred指向尾节点,node指向新的节点。,
,node.prev = pred;将node的前驱节点设置为pred指向的节点,
,compareAndSetTail(pred, node)通过CAS的方式尝试将当前节点node设置为尾结点,此处我们假设设置成功,则FIFO队列的tail指向node节点。,
,pred.next = node;将pred节点的后继节点设置为node节点,此时node节点成功进入FIFO队列尾部。,
,而当pred为空,即队列中没有节点或将node节点设置为尾结点失败时,会走enq()方法。我们列举的例子就符合pred为空的情况,就让我们以例子为基础继续分析吧。,进入死循环,首先会走if方法的逻辑,通过CAS的方式尝试将一个新节点设置为head节点,然后将tail也指向新节点。可以看出队列中的头节点只是个初始化的节点,没有任何意义。,
,继续走死循环中的代码,此时t不为null,所以会走else方法。将node的前驱节点指向t,通过CAS方式将当前节点node设置为尾结点,然后将t的后继节点指向node。此时线程2的节点就被成功塞入FIFO队列尾部。,
,将已经在队列中的node尝试去获取锁否则挂起。,这里又出现了一次死循环,首先获取当前节点的前驱节点p,如果p是头节点(头节点没有意义),说明node是head后的第一个节点,此时当前获取锁资源的线程1可能会释放锁,所以线程2可以再次尝试获取锁。,假设获取成功,证明拿到锁资源了,将node节点设置为head节点,并将node节点的pre和thread设置为null。因为拿到锁资源了,node节点就不需要排队了。,将头节点p的next置为null,此时p节点就不在队列中存在了,可以帮助GC回收(可达性分析)。failed设置为false,表明获取锁成功;interrupted为false,则线程不会中断。,
,如果p不是head节点或者没有拿到锁资源,会执行下边的代码,因为我们的线程1没有释放锁资源,所以线程2获取锁失败,会继续往下执行。,只有节点的状态为-1,才会唤醒后一个节点,如果节点状态未设置,默认为0。,图解一下ws>0的过程,因为ws>0的节点为失效节点,所以do...while中会重复向前查找前驱节点,直到找到第一个ws<=0的节点为止,将node节点挂到该节点上。,
,我们的pred是头结点且未设置状态,所以状态为0,会走else。通过CAS尝试将pred节点的waitStatus设置为-1,表明node节点需要被pred唤醒。,
,shouldParkAfterFailedAcquire()返回false,继续执行acquireQueued()中的死循环。,步骤和上边一样,node的前驱节点还是head,继续尝试获取锁。如果线程1释放了锁,线程2就可以拿到,返回true;否则继续调用shouldParkAfterFailedAcquire(),因为上一步已经将前驱结点的ws设置为-1了,所以直接返回true。,执行parkAndCheckInterrupt()方法,通过UNSAFE.park();方法阻塞当前线程2。等以后执行unpark方法的时候,如果node是头节点后的第一个节点,会进入acquireQueued()方法中走if (p == head && tryAcquire(arg))的逻辑获取锁资源并结束死循环。,该方法执行的机率约等于0,为什么这么说呢?因为针对failed属性,只有JVM内部出现问题时,才可能出现异常,执行该方法。,执行到while时找到前驱节点中最近的有效节点,把当前节点node挂到有效节点后边,可以过滤掉当前节点前的失效节点。声明出有效节点的第一个后继无效节点predNext,并把当前的node节点状态设置为失效状态。,
,if中的操作:如果当前节点是尾节点,CAS尝试将最近的有效节点设置为尾节点,并将尾节点的next设置为null。,
,else中的操作:,如果pred节点不是头结点即中间节点,并且pred的waitStatus为-1或者waitStatus<=0,为了让pred节点能唤醒后继节点,需要设置为-1,并且pred节点的线程不为空。获取node节点的后继节点,如果后继节点有效,CAS尝试将pred的next指向node节点的next。,
,当其他节点来找有效节点的时候走当前node的prev这条线,而不是再一个一个往前找,可以提高效率。,如果是头结点则唤醒后继节点。,最后将node节点的next指向自己。,释放锁是不区分公平锁和非公平锁的,释放锁的核心是将state由大于 0 的数置为 0。废话不多说,直接上代码,如果释放锁成功,需要获取head节点。如果头结点不为空且waitStatus不为0,则证明有node在排队,执行唤醒挂起其他node的操作。,我们的例子中线程1占用锁资源,线程1释放锁之后,state为0。进入if操作,将释放标志更新为true,将FIFO队列的exclusiveOwnerThread标志置为null。,
,用于唤醒AQS中被挂起的线程。,问题解析:为什么要从尾结点往前查找呢?,因为在addWaiter方法中是先给prev指针赋值,最后才将上一个节点的next指针赋值,为了避免防止丢失节点或者跳过节点,必须从后往前找。,我们举例中head节点的状态为-1,通过CAS的方式将head节点的waitStatus设置为0。,
,我们的头结点的后继节点是线程2所在的节点,不为null,所以这边会执行unpark操作,从上边的acquireQueued()内的parkAndCheckInterrupt()方法继续执行。,因为线程2未中断,所以返回false。继续执行acquireQueued()中的死循环,此时p是头节点,且能获取锁成功,将exclusiveOwnerThread设置为线程2,即线程2 获取锁资源。,将node节点设置为head节点,并将node节点的pre和thread设置为null。因为拿到锁资源了,node节点就不需要排队了。,将头节点p的next置为null,此时p节点就不在队列中存在了,可以帮助GC回收(可达性分析)。failed设置为false,表明获取锁成功;interrupted为false,则线程不会中断。,
,为什么被唤醒的线程要调用Thread.interrupted()清除中断标记,从上边的方法可以看出,当parkAndCheckInterrupt()方法返回true时,即Thread.interrupted()方法返回了true,也就是该线程被中断了。为了让被唤醒的线程继续执行后续获取锁的操作,就需要让中断的线程像没有被中断过一样继续往下执行,所以在返回中断标记的同时要清除中断标记,将其设置为false。,清除中断标记之后不代表该线程不需要中断了,所以在parkAndCheckInterrupt()方法返回true时,要自己设置一个中断标志interrupted = true,为的就是当获取到锁资源执行完相关的操作之后进行中断补偿,故而需要执行selfInterrupt()方法中断线程。,以上就是我们加锁解锁的图解过程了。最后我们再来说一下公平锁和非公平锁的区别。,前边已经说过了,似乎非公平锁包含了公平锁的全部操作。打开公平锁的代码,我们发现accquire()方法中只有该方法的实现有点区别。,
,hasQueuedPredecessors()返回false时才会尝试获取锁资源。该方法代码实现如下,h==t时,队列为空,表示没人排队,可以获取锁资源;,队列不为空,头结点有后继节点不为空且s节点获取锁的线程是当前线程也可以获取锁资源,代表锁重入操作;,以上就是我们的全部内容了,我们在最后再做个总结:
© 版权声明
文章版权归作者所有,未经允许请勿转载。