话很少说,扶我起来,我还能够继续撸。前端
在学习ReentrantLock源码以前,先来回顾一下链表、队列数据结构的基本概念~~java
小学1、二年级的时候,学校组织户外活动,老师们通常都要求同窗之间小手牵着小手。这个场景就很相似一个单链表。每一个小朋友能够看做一个节点信息,而后经过牵手的方式,造成整个链表结构。node
一、链表是以节点的形式来存储数据,能够称之为:链式存储程序员
二、每一个节点都包含所须要存放对应的数据(data 域),以及指向下一个节点的元素(next 域)。面试
三、链表能够带头节点也能够不带头节点,根据实际需求来肯定,头节点通常不会存放具体数据,只会指向下一个节点。后端
四、链表总的来讲能够分之为几种类型:单链表、双向链表、环形链表(循环链表)设计模式
单链表(带头节点) 结构示意图:数据结构
单链表总的来看,理解比较简单,可是缺点也是显而易见的,查找的方向只能是一个方向,而且在某些操做下,单链表会比较费劲。好比说在删除某个单链表节点时,咱们须要找到删除节点的,前一个节点才可以进行删除。框架
这个时候,就有了咱们双向链表:源码分析
双向链表(带头节点) 结构示意图:
相对应单链表来讲,双向链表多了一个pre属性,这个属性会指向当前节点的上一个节点,因此称之为双向链表。
换句话来讲双向链表就是你中有我,我中有你哈哈哈哈~~~~
环形链表(循环链表)结构示意图:
环形链表也就是,链表最后一个节点,指向了头节点,总体构成一个环形。其实理解了单链表结构,后面两种结构都比较好理解。
队列其实只要记住最重要特色:遵循先入先出的原则,先存入的数据,先取出,后存储的数据后取出。
在换到生活场景来讲,最简单的就是排队,最早排队的人,弄完事最早走了,也就是出队列了。
队列也是线性表的一种,它只容许在表的前端进行进行删除操做,在表的后端进行插入操做。进行删除操做端叫作队头,进行插入的一端叫作队尾。
这里小编多的就不讲了,相信做为一名码农来讲,这两种都是很基本、很基本、很基本的数据结构了。
AQS是什么呢? 全称是AbstractQueuedSynchronizer,中文就是队列同步器,简单暴力来讲,它对应咱们Java中的一个抽象类,AQS是ReentrantLock很重要的实现部分。
首先咱们须要了解到,在AQS中包含了哪些重要内容,小编这里给列举部分出来了。
这里代码小编省略不少了,展现了咱们所须要关心的内容。(省的担忧大家说小编乱画图)
// @author Doug Lea public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { static final class Node { // 指向上一个节点 volatile Node prev; // 指向下一个节点 volatile Node next; // 存放具体的数据 volatile Thread thread; // 线程的等待状态 volatile int waitStatus; } // 头节点 private transient volatile Node head; // 尾节点 private transient volatile Node tail; // 锁状态 private volatile int state; }
假设如今要求小伙伴们本身实现一把锁,大家会怎么去设计一把锁呢?
最容易想到的方案就是,首先确定要有个锁状态(假设就是个int 变量 0 自由状态、1 被锁状态),若是一个线程获取到了锁,就把这个锁状态改为 1,线程释放锁就改为0。 那又假设如今咱们线程一获取到了锁,线程二来了怎么办? 线程二又要去哪里等着呢? 这个时候AQS就给你提供了一系列基本的操做,让开发者更加专一锁的实现。
AQS这种设计属于模板方法模式(行为型设计模式),使用者须要继承这个AQS并重写指定的方法,最后调用AQS提供的模板方法,而这些模板方法会调用使用者重写的方法。
这么说把,AQS是用来构建锁的基础框架,主要的使用方式是继承,子类经过继承AQS并实现它的一系列方法来管理同步状态。还有咱们实现一把锁确定避免不了对锁状态的更改,AQS还提供了如下三个方法:
getState(): 获取当前锁状态
setState(int newState): 设置当前锁状态
compareAndSetState(int expect, int update):CAS设置锁状态,CAS可以保证原子性操做,小编上一篇文章讲sync有具体讲到。
看到这里,但愿小伙伴可以对AQS这个抽象类有个大概的认识。
本文主要注重ReentrantLock 加锁、解锁过程源码分析!!!
本文都是以公平锁为主,若是弄懂了公平锁的过程,再回头过看看非公平锁,就很轻松了,这个就交给小伙伴大家本身了~
总体看下ReentrantLock结构:先来个IDEA里面展现的结构图,而后小编再结合画一个更简单明了的结构图。
重入锁简单来讲一个线程能够重复获取锁资源,虽然ReentrantLock不像synchronized关键字同样支持隐式的重入锁,可是在调用lock方法时,它会判断当前尝试获取锁的线程,是否等于已经拥有锁的线程,若是成立则不会被阻塞(下面讲源码的时候会讲到)。
还有ReentrantLock在建立的时候,能够通构造方法指定建立公平锁仍是非公平锁。这里是个细节部分,若是知道有公平锁和非公平锁,可是不知道怎么建立,这样还敢说看过源码?
// ReentrantLock 构造方法 // 默认非公平锁 public ReentrantLock() { sync = new NonfairSync(); } // 传入true,建立公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
怎么理解公平锁和非公平锁呢? 先对锁进行获取的线程必定先拿到锁,那么这个锁是公平的,反之就是不公平的。
好比:排队买包子,你们都一一排队进行购买那么就是公平的,可是若是有人插队,那就变成不公平了。凭啥你这个后来的还先买包子,就这个意思拉~~
如下就是一个简单锁的演示了,简单的加锁解锁。
public class ReentrantLockTest { public static void main(String[] args) { // 建立公平锁 ReentrantLock lock = new ReentrantLock(true); // 加锁 lock.lock(); hello(); // 解锁 lock.unlock(); } public static void hello() { System.out.println("Say Hello"); } }
既然咱们是看加锁的过程,就从lock方法开始下手呗,前方高能,请注意准备~~~
点进去以后看到了调用了sync对象的lock方法,sync是咱们ReentrantLock中的一个内部类,而且这个sync继承了AQS这个类。
public void lock() { sync.lock(); }
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; // 抽象方法,由公平锁和非公平锁具体实现 abstract void lock(); // ..... 代码省略 }
经过快捷键查看,有两个类对Sync中的lock方法进行了实现,咱们先看公平锁:FairSync
看代码得知,lock方法最后调用了acquire方法,而且传入了一个参数,值为:1,那咱们再继续跟下去~
/** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } // ...... 代码省略 }
这个时候咱们就来到AQS为咱们提供的方法了,接下来小编一一讲解~~
public final void acquire(int arg) { // 第一个调用了tryAcquire方法,这方法判断能不能拿到锁 // 强调,这里的tryAcquire的结果,最后是取反,最前面加了 !运算 if (!tryAcquire(arg) && // 后面的方法,慢慢道来,先保持神秘感 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
从方法名上看,字面意思就是尝试获取,获取什么呢? 那固然是获取锁呀。
从acquire()点击tryAcquire方法进去看,AQS为咱们提供了默认实现,默认若是没重写该方法,则抛出一个异常,这里就很突出模板方法模式这种设计模式的概念,提供了一个默认实现。
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
一样,咱们查看公平锁的实现 ~
最后来到了FairSync对象中的tyrAcquire方法了,重点来啦~~
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } // 尝试获取锁 拿到锁了返回:true,没拿到锁返回:false protected final boolean tryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); // 获取锁状态 , 自由状态 = 0,被上锁 = 1 ,> 1 表示重入 int c = getState(); // 判断当前状态是否等于自由状态 if (c == 0) { // hasQueuedPredecessors 判断本身需不须要排队,这个方法比较复杂,在下面补充部分详细解释,返回值,不须要排队返回false,而后取反,须要排队返回true if (!hasQueuedPredecessors() && // compareAndSetState 若是不须要排队则直接进行CAS尝试加锁,成功则直接方法true compareAndSetState(0, acquires)) { // 成功获取锁,把当前线程设置成锁的拥有者,为了后续方便判断是否是可重入锁 setExclusiveOwnerThread(current); return true; } } // 判断当前线程是否等于锁的持有线程,这里也证实了ReentrantLock是可重入锁 else if (current == getExclusiveOwnerThread()) { // 若是是重复锁,计数器 + 1 int nextc = c + acquires; // 正常来讲nextc不可能会小于0,因而判断若是小于0则直接抛出异常 if (nextc < 0) throw new Error("Maximum lock count exceeded"); // 赋值计数器+1的结果 setState(nextc); // 若是重入成功返回true return true; } // 若是c不等于0,而且当前线程不等于持有锁的线程,直接返回false,由于就表明着有其余线程拿到锁了 return false; } }
tryAcquire方法执行完成,又回到这里: tryAcquire方法拿到锁返回结果:true,没拿到锁返回:false。
一共分两种状况:
第一种状况,拿到锁了,结果为true,经过取反,最后结果为false,因为这里是 && 运算,后面的方法则不会进行,直接返回,代码正常执行,线程也不会进入阻塞状态。第二种状况,没有拿到锁,结果为false,经过取反,最后结果为true,这个时候,if判断会接着往下执行,执行这句代码:acquireQueued(addWaiter(Node.EXCLUSIVE), arg),先执行addWaiter方法。
public final void acquire(int arg) { // tryAcquire执行完,回到这里 if (!tryAcquire(arg) && // Node.EXCLUSIVE 这里传进去的参数是为null,在Node类里面 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
看到这里,还记得咱们AQS里面有个head、tail,以及Node吧,若是印象模糊了,赶忙翻上去看看。
AQS在初始化的时候,数据大概是这个样子的,这个时候队列还没初始化的状态,因此head、tail都是为空。
addWaiiter这个方法总的来讲作了什么事呢?
核心做用:把没有获取到的线程包装成Node节点,而且添加到队列中
具体逻辑分两步,判断队列尾部节点是否是为空,为空就去初始化队列,不为空就维护队列关系。
这里须要小伙伴掌握双向链表数据结构,才能更容易的明白怎么去维护一个队列的关系。
private Node addWaiter(Node mode) { // 由于在AQS队列里面,节点元素是Node,因此须要把当前类包装成一个node节点 Node node = new Node(Thread.currentThread(), mode); // 把尾节点,赋值给pred,这里一共分两种两种状况 Node pred = tail; // 判断尾部节点等不等于null,若是队列没有被初始化,tail确定是个空 // 反而言之,若是队列被初始化了,head和tail都不会为空 if (pred != null) { // 整个就是维护链表关系 // 把当前须要加入队列元素的上一个节点,指向队列尾部 node.prev = pred; // CAS操做,若是队列的尾部节点是等于pred的话,就把tail 设置成 node,这个时候node就是最后一个节点了 if (compareAndSetTail(pred, node)) { // 把以前尾部节点的next指向最后的的node节点 pred.next = node; return node; } } // 初始化队列 enq(node); return node; }
到这里小伙伴要记住:AQS队列默认是没有被初始化的,只有当发生竞争的时候,而且有线程没有拿到锁才会初始化队列,不然队列不会被初始化~
什么状况下不会被初始化呢?
一、线程没有发生竞争的状况下,队列不会被初始化,由tryAcquire方法就能够体现出,若是拿到锁了,就直接返回了。
二、线程交替执行的状况下,队列不会被初始化,交替执行的意思是,线程执行完代码后,释放锁,线程二来了,能够直接获取锁。这种就是交替执行,你用完了,正好就轮到我用了。
这个方式就是为了初始化队列,参数是由addWaiter方法把当前线程包装成的Node节点。
// 整个方法就是初始化队列,而且把node节点追加到队列尾部 private Node enq(final Node node) { // 进来就是个死循环,这里看代码得知,一共循环两次 for (;;) { Node t = tail; // 第一次进来tail等于null // 第二次进来因为下面代码已经把tail赋值成一个为空的node节点,因此t如今不等于null了 if (t == null) { // CAS把head设置成一个空的Node节点 if (compareAndSetHead(new Node())) // 把空的头节点赋值给tail节点 tail = head; } else { // 第二次循环就走到这里,先把须要加入队列的上一个节点指向队列尾部 node.prev = t; // CAS操做判断尾部是否是t若是是,则把node设置成队列尾部 if (compareAndSetTail(t, node)) { // 再把以前链表尾部的next属性,链接刚刚更换的node尾部节点 t.next = node; return t; } } } }
经过enq代码咱们能够得知一个很重要、很重要、很重要的知识点,在队列被初始化的时候,知道队列第一个元素是什么么? 若是你认为是要等待线程的node节点,那么你就错了。
经过这两句代码得知,在队列初始化的时候,是new了一个空Node节点,赋值给了head,紧接着,又把head 赋值给tail。
if (compareAndSetHead(new Node())) // 把空的头节点赋值给tail节点 tail = head;
初始化完成后,队列结构应该是这样子的。
队列初始化后,紧接着第二次循环对不对,t就是咱们的尾部节点,node就是要被加入队列的node节点,也就是咱们所谓要等待的线程的node节点,这里代码执行完后,直接return了,循环终止了。
// 第二次循环就走到这里,先把须要加入队列的上一个节点指向队列尾部 node.prev = t; // CAS操做判断尾部是否是t若是是,则把node设置成队列尾部 if (compareAndSetTail(t, node)) { // 再把以前链表尾部的next属性,链接刚刚更换的node尾部节点 t.next = node; return t; }
看了这幅图,哪怕对双向链表不熟悉,应该也能够看懂了吧, skr skr skr ~~~~
记住,这里队列初始化的时候,第一个元素是空,队列里面存在两个元素,切记切记切记,这也是面试须要注意的细节,把这个点勇敢的、大声的、自信的出说来,确定可以证实你是看过源码的。
好了,最终addWaiter方法会返回一个初始化而且已经维护好,队列关系的Node节点出来。
public final void acquire(int arg) { if (!tryAcquire(arg) && // addWaiter返回Node,紧接着调用acquireQueued 方法 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
看到这里,也就是咱们lock方法的接近尾声了,咱们拿到了队列中的数据,猜猜接下来须要作什么?
既然没拿到锁,就让线程进入阻塞状态,可是确定不是直接就阻塞了,还须要通过一系列的操做,看源码:
// node == 须要进队列的节点、arg = 1 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)) { // 若是能进入到这里,则表明前面那个打饭的人已经搞完了,能够轮第一个排队的人打饭了 // 既然前面那我的打完饭了,就能够出队列了,会把thread、prev、next置空,等待GC回收 setHead(node); p.next = null; // help GC failed = false; // 返回false,整个acquire方法返回false,就出去了 return interrupted; } // 若是不是头部节点,就要过来等待排队了 // shouldParkAfterFailedAcquire 这方法会使当前循环再循环一次,至关于自旋一次获取锁 if (shouldParkAfterFailedAcquire(p, node) && // 队列阻塞,整个线程就等待被唤醒了 parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
看代码得知,若是当前传进来的节点的上一个节点,是等于head,那么又会调用tryAcquire方法,这里体现的就是自旋获取锁,为何要这么作呢? 是为了不进入阻塞的状态,假设线程一已经获取到锁了,而后线程二须要进入阻塞,可是因为线程二还在进入阻塞状态的路上,线程一就已经释放锁了。为了不这种状况,第一个排队的线程,有必要在阻塞以前再次去尝试获取锁。
假设一:假设咱们线程二在进入阻塞状态以前,尝试去获取锁,哎,居然成功了,则会执行一下代码:
// 调用方法,代码在下面 setHead(node); p.next = null; // help GC private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
若是拿到锁了,队列的内容,固然会发送变化,由图可见,咱们会发现一个问题,队列的第一个节点,又是一个空节点。
由于当拿到锁以后,会把当前节点的内容,指针所有赋值为null,这也是个小细节哟。
假设二:若是当前节点的上一个节点,不是head,那么很遗憾,没有资格去尝试获取锁,那就走下面的代码。
在进入阻塞以前,会调用shouldParkAfterFailedAcquire方法,这个方法小编先告诉你,因为咱们这里是死循环对吧,这个方法第一次调用会放回false,返回false则不会执行执行后续代码,再一次进入循环,通过一些列操做,仍是没有资格获取锁,或者获取锁失败,则又会来到这里。当第二次调用shouldParkAfterFailedAcquire方法,会放回ture,这个时候,线程才会调用parkAndCheckInterrupt方法,将线程进入阻塞状态,等待锁释放,而后被唤醒!!
// 若是不是头部节点,就要过来等待排队了 // shouldParkAfterFailedAcquire 这方法会使当前循环再循环一次,至关于自旋一次获取锁 if (shouldParkAfterFailedAcquire(p, node) && // 队列阻塞,整个线程就等待被唤醒了 parkAndCheckInterrupt()) interrupted = true;
private final boolean parkAndCheckInterrupt() { // 在这里被park,等待unpark,若是该线程被unpark,则继续从这里执行 LockSupport.park(this); // 这个是获取该线程是否被中断过,这句代码须要结合lockInterruptibly方法来说,小编就不详细说了,否则一篇文章讲太多了~~~~ return Thread.interrupted(); }
到这里咱们ReentrantLock整个加锁的过程,就至关于讲完啦,可是这才是最最最简单的一部分,由于还有不少场景没考虑到。
上面说为何这个方法第一次调用返回false,第二次调用返回ture,咱们来看源码吧~~
这个方法主要作了一件事:把当前节点的,上一个节点的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) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; // 判断是否大于0,waitStatus分几种状态,这里其余几种状态的源码就不一一讲了。 // = 1:因为在同步队列中等待的线程,等待超时或者被中断,须要从同步队列中取消等待,该节点进入该状态不会再变化 // = -1:后续节点的线程处于等待状态,而当前节点的线程若是释放了同步状态或者被取消,将会通知后续节点,使后续节点继续运行 // = -2:节点在等待队列中,节点线程在Condition上,当其余线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到同步状态中获取 // = -3:表示下一次共享式同步状态获取将会无条件地被传播下去 // = 0 :初始状态 if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; // 由于默认0,因此第一次会走到else方法里面 } else { // CAS吧waitStatus修改为-1 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } // 返回false,外层方法接着循环操做 return false; }
讲完加锁过程,就来解锁过程吧,说实话,看源码这种经历,必需要本身花时间去看,去作笔记,去理解,大脑最好有个总体的思路,这样才会印象深入。
public class ReentrantLockTest { public static void main(String[] args) { // 建立公平锁 ReentrantLock lock = new ReentrantLock(true); // 加锁 lock.lock(); hello(); // 解锁 lock.unlock(); } public static void hello() { System.out.println("Say Hello"); } }
点击unlock解锁的方法,会调用到release方法,这个是AQS提供的模板方法,再来看tryRelease方法。
public void unlock() { sync.release(1); }
public final boolean release(int arg) { // tryRelease 释放锁,若是真正释放会把当前持有锁的线程赋值为空,不然只是计数器-1 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
发现又是个抽象类,咱们选择ReentrantLock类实现的
这里要注意:
一、当前解锁的线程,必须是持有锁的线程
二、state状态,必须是等于0,才算是真正的解锁,不然只是表明重入次数-1.
protected final boolean tryRelease(int releases) { // 获取锁计数器 - 1 int c = getState() - releases; // 判断当前线程 是否等于 持有锁的线程,若是不是则抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // 返回标志 boolean free = false; // 若是计算器等于0,则表明须要真正释放锁,不然是表明重入次数-1 if (c == 0) { free = true; // 将持有锁的线程赋值空 setExclusiveOwnerThread(null); } // 从新设置state状态 setState(c); return free; }
执行完tryRelease方法,返回到release,进行if判断,若是返回false,就直接返回了,不然进行解锁操做。
public final boolean release(int arg) { // tryRelease方法返回true,则表示真的须要释放锁 if (tryRelease(arg)) { // 若是是须要真正释放锁,先获取head节点 Node h = head; // 第一种状况,假设队列没有被初始化,这个时候head是为空的,则不须要进行锁唤醒 // 第二种状况,队列被初始化了head不为空,而且只要有线程在队列中排队,waitStatus在被加入队列以前,会把当前节点的上一个节点的waitStatus改成-1 // 因此只有知足h != null && h.waitStatus != 0 这个条件表达式,才能真正表明有线程正在排队 if (h != null && h.waitStatus != 0) // 解锁操做,传入头节点信息 unparkSuccessor(h); return true; } return false; }
这里的参数传进来的是head的node节点信息,真正解锁的线程是head.next节点,而后调用unpark进行解锁。
private void unparkSuccessor(Node node) { // 先获取head节点的状态,应该是等于-1,缘由在shouldParkAfterFailedAcquire方法中有体现 int ws = node.waitStatus; // 因为-1会小于0,因此从新改成0 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); // 获取第一个正常排队的队列 Node s = node.next; // 这里涉及到其余场景,小编就不详细讲了,正常的解锁不会执行这里 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() { // 在这里被park,等待unpark,若是该线程被unpark,则继续从这里执行 LockSupport.park(this); // 这个是获取该线程是否被中断过,这句代码须要结合lockInterruptibly方法来说,小编就不详细说了,否则一篇文章讲太多了~~~~ return Thread.interrupted(); }
看到这里,小编但愿是小伙伴真的理解了ReentrantLock加锁和解锁的过程,而且在内心有总体流程,否则你看这个方法,会很蒙,这个方法虽然代码几行,可是要彻底理解,比较困难。
这个方法估计是ReentrantLock加锁过程当中,最为复杂的一个方法了,因此放到了最后来说~~~
// 不要小看如下几行代码,涉及的场景比较复杂 public final boolean hasQueuedPredecessors() { // 分别把尾节点、头节点赋值给 t、h Node t = tail; Node h = head; Node s; // AQS队列若是没有发生竞争,刚开始都是未初始化的,因此一开始tail、head都是为null // 第一种状况: AQS队列没有初始化的状况 // 假设线程一,第一个进来,这个时候t、h都是为null,因此在h != t,这个判断返回false,因为用的是&&因此整个判断返回fase // 返回flase表示不须要排队。 // 可是也不排除可能会有两个线程同时进来判断的状况,假设两个线程发现本身都不须要排队,就跑去CAS进行修改计数器,这个时候确定会有一个失败的 // CAS 是能够保证原子性操做的,假设线程一它CAS成功了,那么线程二就会去初始化队列,老老实实排队去了 // 第二种状况: AQS队列被初始化了 // 场景一:队列元素大于1个点状况,假设有一个线程在排队,在队列中应该有而个元素,一个是头节点、线程2 // 如今线程2以前的线程已经执行完,而且释放锁唤醒线程2.线程2又会继续醒来循环。而且线程二是第一个排队的,因此有资格获取锁 // 只要是获取锁就会来排队需不须要排队,代码又回到这里 // 如今 h = 等于头节点,而 tail = 线程2的node节点,因此 h != t 结果为true // h表示头节点,而且h.next是线程2的节点,因此 (s = h.next) == null 返回 flase // s 等于 h.next,也就是线程2的节点信息,而且当前执行的线程也是线程2,因此 s.thread != Thread.currentThread(),返回false // 最后return的结果是 true && false,结果为false,表明不须要排队 // 场景二:队列元素等于1个,什么状况下队列被初始化了而且只有一个元素呢? // 当有线程竞争初始化队列,以后队列又所有都被消费完了。最后剩下一个为空的node,而且head和tail都指向它 // 这个时候有新的线程进来,其实h != t,直接返回false,由于head 和tail都指向最后一个节点了 return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
小编按照本身的思路,尽本身有限的能力,表示了出来。
至于要理解,真的须要小伙伴仔细去思考,不断的理解,才会造成本身的思路, 奥利给~~~~~
这里整个ReentrantLock源码加锁解锁的流程图,小编也没贴出来,靠小伙伴们去完成吧~~~~~
这篇文章说了这么多,看了这么多源码,但实际上才是ReentrantLock很基本的东西。
这就引发一个思考,一个小小的ReentrantLock想要完彻底全的去精通每行代码,须要咱们花大量的时间、精力去研究,去探讨。 更况且做为一名程序员,所须要掌握的技术,大家懂得,好像看不到尽头。 真的就是,你知道的越多,就会发现你不知道得就越多。
对于正在走向程序员道路上的大家,或者已经码代码几年的程序员们,大家是选择继续坚持,和代码死磕到底,一直到年龄的瓶颈、仍是中途就选择转行呢? 能够文章尾部评论哟。
点赞是大家给博主最大的动力哟 👇👇👇👇👇👇