当前位置:数据分析 > 【第385期】面试官:深入谈谈你对ReentrantLock的认识

【第385期】面试官:深入谈谈你对ReentrantLock的认识

  • 发布:2023-10-05 07:18

1.简介 话不多说,扶我起来,让我继续手淫吧。 在学习ReentrantLock的源码之前,我们先回顾一下链表和队列数据结构的基本概念~~ 2. 数据结构 2.1 链表 小学一二年级,学校组织户外活动,老师通常要求学生互相牵手。这种情况与单链表非常相似。每个小朋友都可以看成是一个节点信息,然后牵手就形成了整个链表结构。 1、链表以节点的形式存储数据,可以称为:链式存储 2、每个节点包含对应的需要存储的数据(data field),以及指向下一个节点的元素(next field)。 3、链表可以有头节点,也可以没有,根据实际需要确定。头节点一般不存储具体数据,只指向下一个节点。 4、一般来说,链表可以分为几种:单链表、双向链表、循环链表(循环链表)。 单链表结构图(引导节点): 一般来说,单链表比较容易理解,但缺点也很明显。搜索方向只能是一个方向,在某些操作下,单向链表会比较困难。例如,删除单链表节点时,我们需要找到被删除的节点,以便删除前一个节点。 这时候,我们就有了我们的双向链表。 双向链表结构图(引导节点): 与单链表相对应,双向链表多了一个pre属性,指向当前节点的前一个节点,因此称为双向链表。 换句话说,双向链表就是你在我里面,我在你里面哈哈哈哈~~~~ 循环链表的结构示意图: 循环链表是指链表的最后一个节点指向头节点,整体形成一个环。其实了解了单链表结构后,后两种结构就更容易理解了。 2.2 队列 其实只要记住队列最重要的特性:遵循先进先出原则,先存的数据先取出,后存的数据后取出。 就生活情况而言,最简单的就是排队。第一个排队的人完成工作后将第一个离开,即退出队列。 队列也是线性表的一种。它只允许在表前端进行删除操作,在表后端进行插入操作。执行删除操作的一端称为队列头,执行插入操作的一端称为队列尾。 我不会在这里讨论太多细节。我相信作为一个程序员,这两个都是非常基础、非常基础、非常基础的数据结构。 3.AQS队列同步器 3.1 基本介绍什么是AQS?全称是AbstractQueuedSynchronizer,中文意思是队列同步器。简单来说,它对应的是我们java中的一个抽象类。 AQS是ReentrantLock非常重要的实施部分。 首先,我们需要了解AQS包含哪些重要内容。编辑在这里列出了其中一些。 小编这里省略了很多代码来展示我们需要关心的内容。 (省得我担心你说小编乱画了) // @author Doug Leapublic 抽象类 AbstractQueuedSynchronizer 扩展了 AbstractOwnableSynchronizer 实现 java.io.Serialized { static Final class Node { // 指向前一个节点 Volatile Node prev; // 指向下一个节点 Volatile Node next; // 存储具体数据 volatile Thread thread; // 线程的等待状态 volatile int waitStatus; } // 头节点私有瞬态易失性节点头; // 尾节点私有瞬态易失性节点尾部; // 锁定状态 private volatile int state; } 3.2 AQS在ReentrantLock内部扮演什么角色? 假设现在要求您自己实现一个锁。你会如何设计一把锁? 最容易想到的解决方案是首先必须有一个锁定状态(假设它是一个具有 0 个自由状态和 1 个锁定状态的 int 变量)。如果有线程获取锁,则将锁状态更改为1,然后该线程释放锁。把它改成0。那么假设现在我们的线程一已经获得了锁,如果线程二来了怎么办?线程二将在哪里等待?此时,AQS为您提供了一系列基础操作,让开发者可以更加专注于锁的实现。 AQS的设计属于模板方法模式(行为设计模式)。用户需要继承这个AQS并重写指定的方法,最后调用AQS提供的模板方法,而这些模板方法会调用用户重写的方法。这么说吧,AQS就是用来构建锁的基础框架。主要的使用方法是继承。子类通过继承AQS并实现其一系列方法来管理同步状态。另外,我们在实现锁的时候,肯定不会避免锁状态的改变。 AQS还提供了以下三种方法: getState():获取当前锁状态 setState(int newState):设置当前锁状态 CompareAndSetState(int Expect, int update):CAS设置锁状态,CAS可以保证原子操作 看到这里,希望大家能够对抽象类AQS有一个大概的了解。 4、ReentrantLock加锁流程源码分析 本文主要针对ReentrantLock加锁解锁过程源码分析! ! ! 本文主要关注公平锁。如果了解了公平锁的流程,回头看非公平锁就会非常容易。这个就留给各位朋友啦~ 4.1 ReentrantLock的结构图 我们先看一下ReentrantLock的整体结构:首先我们看一下创意中展示的结构图,然后小编会在此基础上画出更加简单清晰的结构图。 4.2 ReentrantLock重新上锁 可重入锁简单来说就是一个线程可以重复获取锁资源。虽然ReentrantLock不支持像synchronized关键字那样的隐式可重入锁,但是在调用lock方法时,会判断当前尝试获取锁的线程是否等于拥有锁的线程,如果成立则不会被阻塞(我们在下面讲源码的时候会讲到)。 并且ReentrantLock创建的时候可以通过构造方法指定是创建公平锁还是非公平锁。这是详细部分。如果你知道有公平锁和非公平锁,但不知道如何创建,你敢说你读过源码吗? // ReentrantLock 构造方法 // 默认非公平锁 public ReentrantLock() {sync = new NonfairSync(); } // 传入 true 创建公平锁 public ReentrantLock (boolean fair) {sync = fair ? new FairSync(): new NonfairSync(); } 如何理解公平锁和非公平锁?先获取锁的线程必须先获取锁,那么锁才是公平的,反之亦然。 比如:排队买包子的时候,如果大家都一一排队买,那是公平的,但如果有人插队,那就不公平了。你这个迟到的人为什么先买馒头?就是你的意思~~ 4.3 锁定方法 下面是一个简单的锁的演示,简单的加锁和解锁。public class ReentrantLockTest { public static void main(String[] args) { // 创建公平锁 ReentrantLock lock = new ReentrantLock(true); // 添加锁 lock.lock();你好(); // 解锁 Lock.unlock(); } public static void hello() { System.out.println("打个招呼"); }} 既然我们正在研究加锁的过程,那么我们就从lock方法开始吧。前方能量很大,请做好准备~~~ 点击进去后,看到调用了sync对象的lock方法。 sync是我们ReentrantLock中的一个内部类,这个sync继承了AQS类。 公共无效锁(){同步。锁(); } 抽象静态类同步扩展AbstractQueuedSynchronizer { 私有静态最终长serialVersionUID = -5179523762034025860L; // 抽象方法,具体由公平锁和非公平锁实现abstract void lock(); 。 翻看快捷键,Sync中有两个类实现了lock方法。我们先看一下公平锁:FairSync 看代码我们知道lock方法最终调用了acquire方法,传入了一个值为:1的参数,那么我们继续吧~/** * 公平锁的同步对象 */ static Final class FairSync extends Sync { private static Final long serialVersionUID = -3000897897090466540L;最终无效锁(){获取(1); 这时候我们就来到了AQS提供的方法。接下来小编就为大家一一讲解一下~~ public final void acquire(int arg) { // 第一个调用 tryAcquire 方法。该方法判断是否可以获得锁。 // 强调这里tryAcquire的结果是在最后取反,在前面加上!操作 if (!tryAcquire(arg) && 4.3 tryAcquire方法 从方法名来看,字面意思就是尝试获取,获取什么?那当然是获取锁了。 点击acquire()中的tryAcquire方法可以看到AQS为我们提供了默认的实现。如果默认情况下不重写该方法,则会抛出异常。这里重点强调了模板方法模式的设计模式概念,并提供了一个默认值。完成。 protected boolean tryAcquire(int arg) { 抛出新的 UnsupportedOperationException(); } 同样,我们看一下公平锁的实现~ 最后,我们来到 FairSync 对象中的 tyrAcquire 方法。重点来了~~静态最终类FairSync扩展同步{私有静态最终长serialVersionUID = -3000897897090466540L;最终无效锁(){获取(1); }             // 尝试获取锁,如果获取到锁,返回:true,如果没有获取锁,返回:false            protected final boolean tryAcquire(int acquires) { // 获取当前线程 Final Thread current = Thread.currentThread(); // 获取锁状态,free status = 0,locked = 1,>1表示重新进入 int c = getState() ;否定,需要排队并返回true if (!hasQueuedPredecessors() && {                                                                                        线程被设置为锁的所有者。为了后续方便,判断锁是否可重入 setExclusiveOwnerThread(current); return true;// 判断当前线程是否等于持有锁的线程。这也证明ReentrantLock是一个可重入锁。 else if (current == getExclusiveOwnerThread()) { // 如果是重复锁,counter + 1 int nextc = c + acquires ; // 赋值计数器的结果 + 1 setState (NextC); // 如果返回 TRUE RETURN TRUE; } // 如果 C 不等于 0,且当前线程不等于持有锁的线程 返回 false; }} 这里执行并返回 TRYACQUIRE 方法:Tryacquire 方法获取锁返回结果:true,无锁返回:false。 有两种情况: 第一种情况,获得锁,结果为true。通过反转,最终的结果是假的。由于这是&&操作,所以后面的方法不会执行,直接返回。代码会正常执行,线程不会被阻塞。状态。 第二种情况,没有获得锁,结果为false。通过否定它,最终的结果就是真实的。这时候if判断会继续执行,执行这段代码: acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ,首先执行addWaiter方法。public Final void acquire(int arg) { // tryAcquire执行后,返回这里 if (!tryAcquire(arg) && (Node.EXCLUSIVE), arg)) selfInterrupt(); } 4.4 addWaiter方法 看到这里,记住我们的AQS中有一个head,tail,还有Node。如果印象模糊,赶紧翻起来看一下。 当AQS初始化时,数据如下所示。此时队列还没有初始化,所以头和尾都是空的。 addWaiiter 方法一般做什么? 核心功能:将未获取的线程打包到Node节点中,并添加到队列中 具体逻辑分为两步。判断队列尾节点是否为空。如果为空,则初始化队列。如果不为空,则维护队列关系。 这里需要掌握双向链表数据结构,以便更容易理解如何维护队列关系。private Node addWaiter(Node mode) { // 因为在AQS队列中,节点元素是Node,所以需要将当前类包装成节点node Node node = new Node(Thread.currentThread(), mode); // Tail节点,分配给pred,这里有两种情况 Node pred = tail; // 判断尾节点是否不等于null。如果队列还没有初始化,tail一定为空 // 相反,如果队列已经初始化了,head和tail都不会为空 if (pred != null) { // 整个就是为了维护链接列表关系 // 将当前需要添加到队列元素的前一个节点指向队列尾部 node.prev = pred; // CAS操作,如果队列的尾节点等于pred,则将tail设置为node。此时node就是最后一个节点。 if (compareAndSetTail(pred, node)) { // 将前一个尾节点的下一个节点指向最后一个节点。 www.sychzs.cn = 节点; 。 这里朋友们要记住:AQS队列默认是没有初始化的。只有当发生竞争并且有线程没有获得锁时才会初始化队列,否则队列不会被初始化~ 什么情况下不会被初始化? 1.当线程之间没有竞争时,队列不会被初始化。这可以通过tryAcquire方法来体现。如果获得锁则直接返回。 2、线程交替执行时,队列不会被初始化。交替执行是指线程执行完代码后,释放锁。当第二个线程到来时,可以直接获取锁。这是交替执行。等你用完了,就轮到我用了。 4.5 查询方法 该方法是初始化队列,参数是addWaiter方法封装的当前线程的Node节点。// 整个方法就是初始化队列,并将node节点追加到队列尾部 private Node enq(final Node node) { // 进来就是一个死循环,看这里的代码就知道了总共循环两次 for (;;) { Node t = tail; // 第一次进来时,tail 等于 null // 第二次进来时,由于下面的代码将 tail 赋给了一个空节点,所以现在 t 不等于 null if (t == null) { /cas将head设置为空节点node if (compareandSethead(new node())) //将空头节点赋值给tail节点 tail = head;}​​ else {//第二个循环会来到这里,First指向最后一个需要加入队列的节点node.prev = T; // Cas判断尾部是否为T,如果是,则将该节点设置为队列尾部 IF(compareandSettail(T, node)) {//将上一个链表末尾的next属性连接到刚刚被替换的节点的尾节点 www.sychzs.cn = node;返回t; } } } }} 通过enq代码,我们可以学到一个非常重要、非常重要、非常重要的知识点。当队列初始化的时候,你知道队列的第一个元素是什么吗?如果你认为node节点正在等待线程,那么你就错了。 从这两行代码我们知道,队列初始化的时候,新增了一个空的Node节点,并分配给了head。然后,将头部分配给尾部。 if (compareAndSetHead(new Node())) // 将空头节点分配给尾节点 tail = head; 初始化完成后,队列结构应该是这样的。队列初始化之后,接下来就是第二个循环了,对吧? t是我们的尾节点,node是要添加到队列中的node节点,也就是我们所说的线程等待的node节点。这里的代码执行完后,直接返回,循环结束。 // 第二个循环会来到这里。首先将需要加入队列的前一个节点指向队列尾部node.prev = t; // CAS操作判断tail是否为t。如果是,则将该节点设置为队列尾部 if (compareAndSetTail(t, node)) { // 然后将上一个链表末尾的next属性连接到刚刚替换的节点的尾节点 www.sychzs.cn =节点;返回t;} 看完这张图,即使你对双向链表不熟悉,也应该能够理解。 skr skr skr ~~~~ 请记住,这里初始化队列时,第一个元素为空,队列中有两个元素。记住,记住,记住,这也是面试时需要注意的一个细节。勇敢、大声、自信地说出这句话。绝对可以证明你已经读过源码了。 好了,最后addWaiter方法会返回一个初始化并维护的具有队列关系的Node节点。 公共最终无效获取(int arg){ if(!tryAcquire(arg)&& selfInterrupt(); } 4.5 acquireQueued方法 看到这里,我们的lock方法就到此结束了。我们已经在队列中获取了数据。猜猜接下来需要做什么? 既然没有获得锁,就允许线程进入阻塞状态,但肯定不是直接阻塞。还需要经过一系列的操作。查看源代码:// node == 需要排队的节点,arg = 1final boolean acquireQueued(final Node node, int arg) { // A mark boolean failed = true; try { // 标记布尔值中断 = false; // 这里又来了一个无限循环 for (;;) { // 获取当前节点的前一个元素,要么是头节点,要么是排队等待的其他节点 Final Node p = node.predecessor(); // 判断是否是头节点,如果是头节点,则说明当前节点是队列中的第一个,有资格获取锁,即自旋获取锁。我说要拿锁//就像在自助餐厅提供食物一样。有人正在上菜,排队的第一个人可以去看看前面的人是否吃完//因为他前面没有人排队。后面的人就不一样了。前面的人还在排队,只能老老实实的排队。煮完饭的人可以轮流排队的第一个人做饭 // 现在前面的人已经煮完了,他就可以离开队列了,并且会将thread、prev、next留空,等待GC回收setHead(node); www.sychzs.cn = null; // 帮助 GC 失败 = false; // 返回false,整个acquire方法返回false,退出return中断; // 如果不是头节点,就得来排队等候                        /shouldParkAfterFailedAcquire 该方法会让当前循环再次循环,相当于自旋一次来获取锁。 if (shouldParkAfterFailedAcquire(p, node) &&           Interrupted = true; {If (failed) Cancellacquire(NODE);}} 看代码就知道,如果当前传入的前一个节点,等于head,那么Tryacquire方法会被调用,怎么办? 为了避免进入阻塞状态,假设线程一已经获取了锁,然后线程二需要进入阻塞状态。然而,由于线程二仍在进入阻塞状态的路上,线程一已经释放了锁。为了避免这种情况,第一个排队的线程有必要在阻塞之前再次尝试获取锁。 假设1:假设我们的线程2在进入阻塞状态之前尝试获取锁。唉,成功了,会执行下面的代码: // 调用该方法,代码如下 setHead(node); www.sychzs.cn = null; // 帮助GC private void setHead(Node node) { head = node;节点.thread = null;节点.prev = null; } 如果获得了锁,队列的内容当然会被改变。从图中可以看出,我们会发现一个问题。队列的第一个节点也是空节点。 因为当获得锁时,当前节点的所有内容和指针都会被赋值为null。这也是一个小细节。 假设2:如果当前节点的前一个节点不是head,那么不幸的是,你没有资格尝试获取锁,那么使用下面的代码。在进入阻塞之前,会调用shouldParkAfterFailedAcquire方法。这个方法的小编会先告诉你,因为我们这里有一个无限循环,对吧?第一次调用该方法将返回 false。如果返回 false,则后续代码将不会被执行。再次进入循环,经过一系列操作后,如果仍然没有资格获取锁,或者获取锁失败,就会再次来到这里。 第二次调用shouldParkAfterFailedAcquire方法时,返回true。这时候线程就会调用parkAndCheckInterrupt方法,让线程进入阻塞状态,等待锁被释放,然后被唤醒! ! // 如果不是头节点,就得过来排队等待 // shouldParkAfterFailedAcquire 这个方法会让当前循环再次循环,相当于自旋一次获取锁 if (shouldParkAfterFailedAcquire(p, node) && // 队列被阻塞,整个线程只等待被唤醒 ParkAndCheckInterrupt()) Interrupted = true; 4.5 ParkAndCheckInterrupt方法 private Final boolean parkAndCheckInterrupt() { // 停在这里等待unpark。如果线程被unparked,则从这里继续执行。 LockSupport.park(this); // 这是获取线程是否被中断。这段代码需要和lockInterruptically方法结合起来。小编就不赘述了,不然一篇文章就太多了~~~~ return Thread.interrupted(); } 至此,ReentrantLock的整个加锁过程就相当于结束了,但这只是最简单的部分,因为还有很多场景没有考虑到。 4.6 补充说明:shouldParkAfterFailedAcquire方法 上面解释了为什么此方法在第一次调用时返回 false,而在第二次调用时返回 true。我们来看看源码吧~~ 该方法主要做了一件事:将当前节点和上一个节点的waitStatus状态更改为-1。 当线程进入阻塞状态时,不会将自己的状态变为等待状态,而是会被后面的节点修改。细节,细节,细节例如:你躺在床上睡觉,然后就睡着了。这个时候你能告诉别人你睡着了吗?当然不是,因为你已经睡着了,鼾声如雷。怎么能告诉别人呢。只有当下一个人过来看到你睡得很香的时候,才能告诉别人你在睡觉。//pred为当前前一个节点,node为当前节点 private static boolean ShouldParkAfterFailedAcquire(Node pred, Node node) { //第一个循环中:获取前一个节点的线程状态,默认为0 //第二个循环进去,这个状态变成-1,int ws = pred.waitStatus; // 判断是否等于-1,第一个为0,将waitStatus状态改为-1,代码在else // 第二个为-1,直接返回true,是当前线程正在阻塞 if (ws == Node.SIGNAL) /* * * 该节点已经设置状态,请求释放 * 向其发出信号,以便它可以安全地停放。 */返回真; // 判断是否大于0,waitStatus分为几种状态。其他状态的源码这里就不一一讨论了。 // = 1:因为同步队列中等待的线程超时或被中断,需要从同步队列中取消等待。节点进入该状态后不会发生变化。 // = -1:后续节点的线程处于等待状态。 ,如果当前节点的线程释放同步状态或者被取消,则会通知后续节点,以便后续节点继续运行 // = -2: 该节点在等待队列中,该节点线程开启Condition,当其他线程调用Condition调用signal()方法后,该节点将从等待队列转移到同步队列,并添加到同步状态获取中 // = -3: 表示下次共享同步状态获取将被无条件传播 // = 0: 初始状态 if (ws > 0) { /* * 前驱被取消。跳过前一个,* 表示重试。 */ 执行 { 节点。上一个 = pred = pred.prev;while (pred.waitStatus > 0); www.sychzs.cn = 节点; // 因为默认是0,所以第一次会去else方法 } else { // CAS将waitStatus改为-1compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } // 返回false,外层方法则循环操作 return false;} 5、ReentrantLock解锁过程源码分析 5.1 解锁方法 说完加锁流程,我们来看看解锁流程。说实话,阅读源码的体验需要你花时间去阅读、做笔记、理解。最好让大脑有一个整体的想法,这样才会印象深刻。 。 public class ReentrantLockTest { public static void main(String[] args) { // 创建公平锁 ReentrantLock lock = new ReentrantLock(true); // 添加锁 lock.lock();你好(); // 解锁 Lock.unlock(); } public static void hello() { System.out.println("打个招呼"); }} 点击unlock方法会调用release方法。这是AQS提供的模板方法。我们来看看tryRelease方法。 公共无效解锁(){同步。释放(1); }public Final boolean release(int arg) { // tryRelease 释放锁。如果确实释放了,那么当前持有锁的线程会被赋予一个空值,否则它只是一个-1的计数器 if (tryRelease(arg)) { Node h = head; } if ( h != null && h.waitStatus != 0) unparkSuccessor(h);返回真;返回假;} 5.1 tryRelease方法 发现又是一个抽象类,我们选择ReentrantLock类来实现它 这里请注意: 1、当前解锁的线程一定是持有锁的线程。 2、state必须等于0才算真正解锁,否则只是代表重入次数-1。 protected final boolean tryRelease(intreleases) { // 获取锁计数器 - 1 int c = getState() -releases; // 判断当前线程是否等于持有锁的线程,如果不等于则抛出异常 if (Thread.currentThread( ) != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // 返回标志 boolean free = false; // 如果计算器等于0,则表示需要释放锁,否则表示可重入次数-1 if (c == 0) { free = true; // 将持有锁的线程赋值为null setExclusiveOwnerThread(null); } // 重置状态 setState(c);免费返回;} 执行完tryRelease方法后,返回release并进行if判断。如果返回false则直接返回,否则执行解锁操作。public Final boolean release(int arg) { // tryRelease方法返回true,表示确实需要释放锁 if (tryRelease(arg)) { // 如果需要释放锁,先获取头节点Node h = 头; //第一种情况,假设队列还没有初始化,此时头为空,不需要唤醒锁。 // 第二种情况,队列初始化,头不为空,只要队列中有线程在排队,在waitStatus添加到队列之前,就会改变当前的上一个节点的waitStatus node to -1 // 所以只有满足条件表达式 h != null && h.waitStatus != 0 ,才能真正代表有线程正在排队 if (h != null && h.waitStatus != 0) / /解锁操作,传入头节点信息unparkSuccessor(h);返回真;返回假;} 5.2 unparkSuccessor方法 这里的参数传入的是head的node节点信息。真正解锁的线程是www.sychzs.cn节点,然后调用unpark对其进行解锁。private void unparkSuccessor(Node node) { // 先获取头节点的状态,应该等于-1,原因体现在shouldParkAfterFailedAcquire方法中 int ws = node.waitStatus; // 由于-1将小于0,所以将其更改为0 if (ws < 0)        compareAndSetWaitStatus(node, ws, 0);      // 获取第一个正常排队的队列     Node s = www.sychzs.cn;        // 这里涉及到其他场景,小编就不详细讲了,正常的解锁不会执行这里    if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0)            s = t; } // 通常第一个排队节点不应该为空,所以直接唤醒第一个排队线程 if (s != null) LockSupport.unpark(s.thread);} 如果这里调用了unpark,线程就会被唤醒,会继续执行这个方法。到这里小编就讲解完了整个解锁过程。 private Final boolean parkAndCheckInterrupt() { // 停在这里等待unpark。如果线程被unparked,则从这里继续执行。 LockSupport.park(this); // 这是获取线程是否被中断。这段代码需要和lockInterruptically方法结合起来。小编就不赘述了,不然一篇文章就太多了~~~~ return Thread.interrupted(); } 6.最后的难点:补充说明:hasQueuedPredecessors方法 看到这里,小编希望小伙伴们能够真正了解ReentrantLock加锁和解锁的过程,心中有一个整体的过程。不然你看这个方法的时候会一头雾水。虽然这个方法只有几行代码,但是你必须完全理解它。 ,更加困难。 这个方法可能是ReentrantLock加锁过程中最复杂的一个,所以最后再讨论吧~~~// 不要小看下面这几行代码,涉及到的场景比较复杂 public final boolean hasQueuedPredecessors() { // 将尾节点和头节点分别赋值给t、h Node t = tail;节点 h = 头;节点 s; // 如果AQS队列没有竞争,那么一开始就未初始化,所以一开始tail和head就为null // 第一种情况:AQS队列没有初始化 // 假设线程一来了首先,此时t和h都为null,所以当h != t时,这个判断返回false。由于使用了&&,所以整个判断返回false // 返回false表示不需要排队。 // 但也不排除可能有两个线程同时进来判断。假设两个线程都发现不需要排队,就跑到CAS去修改计数器。这时候肯定会失败 // CAS没问题 为了保证原子操作,假设线程一CAS成功,那么线程二就会初始化队列,老老实实排队 // 第二种场景:AQS队列初始化 //场景一:队列元素大于1 这种情况下,假设有一个线程在排队,队列中应该有2个元素,一个是头节点,线程2 // 现在线程2之前的线程已经执行完毕,并且锁被释放以唤醒线程2。线程2将继续唤醒周期。而线程二是最先排队的,所以它有资格获取锁 // 只要它获取到锁,就会排队。需要排队吗?这里代码返回 // 现在h = 等于头节点,而tail = 线程2节点节点,所以h != t 结果为true // h代表头节点,www.sychzs.cn是线程2的节点, so (s = www.sychzs.cn) == null returns flase // s等于www.sychzs.cn,也是线程2的节点信息,当前执行的线程也是线程2,所以s.thread != Thread .currentThread(), returns false // 最终返回结果为true && false,结果为false,表示不需要排队 /// 场景二:队列元素等于1,什么情况下队列初始化并且只有一个元素? // 当有线程竞争初始化队列时,则所有队列都被消耗。最后剩余一个空节点,head和tail都指向它 // 这时候,有新的线程进来了,其实h != t,直接返回false,因为head和tail都指向最后一个节点。 return h != t && ( (s = www.sychzs.cn) == null || s.thread != Thread.currentThread());} 小编按照自己的想法,尽自己有限的能力表达出来了。 至于理解,确实需要朋友们仔细思考,不断理解,才能形成自己的想法,奥利~~~~~~ 小编这里并没有贴出锁定和解锁ReentrantLock源代码的整个流程图。就交给我的朋友们来完成吧~~~~~~ 7. 程序化生活,你怎么看? 这篇文章说了那么多,看了那么多源码,其实都是关于ReentrantLock的非常基础的东西。 这引发了一个想法。如果一个小ReentrantLock想要完全精通每一行代码,我们需要花费大量的时间和精力去学习和讨论。更重要的是,作为一名程序员,你知道需要掌握的技术,但似乎看不到尽头。确实,你知道的越多,你发现自己不知道的就越多。 对于正在成为程序员路上的你们,或者已经编码了几年的程序员,您是选择继续坚持下去,用代码打拼直到年龄瓶颈,还是选择转行中途?您可以在文章末尾留言。

相关文章