深入浅出 Java 中的神锁:ReentrantLock,还有谁不会?

金歌  金牌会员 | 2023-10-16 01:37:00 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 937|帖子 937|积分 2811

来源:jiannan.blog.csdn.net/article/details/121331360
一、引言

话不多说,扶我起来,我还可以继续撸。
在学习ReentrantLock源码之前,先来回顾一下链表、队列数据结构的基本概念~~
二、数据结构

2.1 链表(Linked List)

小学一、二年级的时候,学校组织户外活动,老师们一般都要求同学之间小手牵着小手。这个场景就很类似一个单链表。每个小朋友可以看作一个节点信息,然后通过牵手的方式,形成整个链表结构。
1、链表是以节点的形式来存储数据,可以称之为:链式存储
2、每个节点都包含所需要存放对应的数据(data 域),以及指向下一个节点的元素(next 域)。
3、链表可以带头节点也可以不带头节点,根据实际需求来确定,头节点一般不会存放具体数据,只会指向下一个节点。
4、链表总的来说可以分之为几种类型:单链表、双向链表、环形链表(循环链表)
单链表(带头节点) 结构示意图:

单链表总的来看,理解比较简单,但是缺点也是显而易见的,查找的方向只能是一个方向,并且在某些操作下,单链表会比较费劲。比如说在删除某个单链表节点时,我们需要找到删除节点的,前一个节点才能够进行删除。
这个时候,就有了我们双向链表。
双向链表(带头节点) 结构示意图:

相对应单链表来说,双向链表多了一个pre属性,这个属性会指向当前节点的上一个节点,所以称之为双向链表。
换句话来说双向链表就是你中有我,我中有你哈哈哈哈~~~~
环形链表(循环链表)结构示意图:

环形链表也就是,链表最后一个节点,指向了头节点,整体构成一个环形。其实理解了单链表结构,后面两种结构都比较好理解。
2.2 队列(Queue)

队列其实只要记住最重要特点:遵循先入先出的原则,先存入的数据,先取出,后存储的数据后取出。
在换到生活场景来说,最简单的就是排队,最先排队的人,弄完事最先走了,也就是出队列了。
队列也是线性表的一种,它只允许在表的前端进行进行删除操作,在表的后端进行插入操作。进行删除操作端叫做队头,进行插入的一端叫做队尾。

这里小编多的就不讲了,相信作为一名码农来说,这两种都是很基本、很基本、很基本的数据结构了。
三、AQS队列同步器

3.1 基本介绍

AQS是什么呢?全称是AbstractQueuedSynchronizer,中文就是队列同步器,简单暴力来说,它对应我们Java中的一个抽象类,AQS是ReentrantLock很重要的实现部分。
首先我们需要了解到,在AQS中包含了哪些重要内容,小编这里给列举部分出来了。

推荐一个开源免费的 Spring Boot 实战项目:
https://github.com/javastacks/spring-boot-best-practice
这里代码小编省略很多了,展示了我们所需要关心的内容。(省的担心你们说小编乱画图)
  1. // @author Doug Lea
  2. public abstract class AbstractQueuedSynchronizer
  3.     extends AbstractOwnableSynchronizer
  4.     implements java.io.Serializable {
  5.    static final class Node {
  6.       // 指向上一个节点
  7.       volatile Node prev;
  8.       // 指向下一个节点
  9.       volatile Node next;
  10.       // 存放具体的数据
  11.       volatile Thread thread;
  12.       // 线程的等待状态
  13.       volatile int waitStatus;
  14.    }
  15.     // 头节点
  16.     private transient volatile Node head;
  17.     // 尾节点
  18.     private transient volatile Node tail;
  19.     // 锁状态
  20.     private volatile int state;
  21. }
复制代码
3.2 AQS在ReentrantLock中起什么作用呢?

假设现在要求小伙伴们自己实现一把锁,你们会怎么去设计一把锁呢?
最容易想到的方案就是,首先肯定要有个锁状态(假设就是个int 变量 0 自由状态、1 被锁状态),如果一个线程获取到了锁,就把这个锁状态改成 1,线程释放锁就改成0。那又假设现在我们线程一获取到了锁,线程二来了怎么办?线程二又要去哪里等着呢? 这个时候AQS就给你提供了一系列基本的操作,让开发者更加专注锁的实现。
AQS这种设计属于模板方法模式(行为型设计模式),使用者需要继承这个AQS并重写指定的方法,最后调用AQS提供的模板方法,而这些模板方法会调用使用者重写的方法。
这么说把,AQS是用来构建锁的基础框架,主要的使用方式是继承,子类通过继承AQS并实现它的一系列方法来管理同步状态。还有我们实现一把锁肯定避免不了对锁状态的更改,AQS还提供了以下三个方法:

  • getState(): 获取当前锁状态
  • setState(int newState): 设置当前锁状态
  • compareAndSetState(int expect, int update):CAS设置锁状态,CAS能够保证原子性操作
看到这里,希望小伙伴能够对AQS这个抽象类有个大概的认识。
四、ReentrantLock 加锁过程源码分析

本文主要注重ReentrantLock 加锁、解锁过程源码分析!!!
本文都是以公平锁为主,如果弄懂了公平锁的过程,再回头过看看非公平锁,就很轻松了,这个就交给小伙伴你们自己了~
4.1 ReentrantLock结构图

整体看下ReentrantLock结构:先来个IDEA里面展示的结构图,然后小编再结合画一个更简单明了的结构图。


4.2 ReentrantLock 重入锁

重入锁简单来说一个线程可以重复获取锁资源,虽然ReentrantLock不像synchronized关键字一样支持隐式的重入锁,但是在调用lock方法时,它会判断当前尝试获取锁的线程,是否等于已经拥有锁的线程,如果成立则不会被阻塞(下面讲源码的时候会讲到)。
还有ReentrantLock在创建的时候,可以通构造方法指定创建公平锁还是非公平锁。这里是个细节部分,如果知道有公平锁和非公平锁,但是不知道怎么创建,这样还敢说看过源码?
  1.     // ReentrantLock 构造方法
  2.     // 默认非公平锁
  3.     public ReentrantLock() {
  4.         sync = new NonfairSync();
  5.     }
  6.     // 传入true,创建公平锁
  7.     public ReentrantLock(boolean fair) {
  8.         sync = fair ? new FairSync() : new NonfairSync();
  9.     }
复制代码
怎么理解公平锁和非公平锁呢?先对锁进行获取的线程一定先拿到锁,那么这个锁是公平的,反之就是不公平的。
比如:排队买包子,大家都一一排队进行购买那么就是公平的,但是如果有人插队,那就变成不公平了。凭啥你这个后来的还先买包子,就这个意思拉~~
4.3 lock方法

以下就是一个简单锁的演示了,简单的加锁解锁。
  1. public class ReentrantLockTest {
  2.     public static void main(String[] args) {
  3.         // 创建公平锁
  4.         ReentrantLock lock = new ReentrantLock(true);
  5.         // 加锁
  6.         lock.lock();
  7.         hello();
  8.         // 解锁
  9.         lock.unlock();
  10.     }
  11.     public static void hello() {
  12.         System.out.println("Say Hello");
  13.     }
  14. }
复制代码
既然我们是看加锁的过程,就从lock方法开始下手呗,前方高能,请注意准备~~~
点进去之后看到了调用了sync对象的lock方法,sync是我们ReentrantLock中的一个内部类,并且这个sync继承了AQS这个类。
  1. public void lock() {
  2.         sync.lock();
  3.     }
  4. abstract static class Sync extends AbstractQueuedSynchronizer {
  5.         private static final long serialVersionUID = -5179523762034025860L;
  6.         // 抽象方法,由公平锁和非公平锁具体实现
  7.         abstract void lock();
  8.         // ..... 代码省略
  9.     }
复制代码
通过快捷键查看,有两个类对Sync中的lock方法进行了实现,我们先看公平锁:FairSync

看代码得知,lock方法最后调用了acquire方法,并且传入了一个参数,值为:1,那我们再继续跟下去~
  1. /**
  2.      * Sync object for fair locks
  3.      */
  4.     static final class FairSync extends Sync {
  5.         private static final long serialVersionUID = -3000897897090466540L;
  6.         final void lock() {
  7.             acquire(1);
  8.         }
  9.         //  ...... 代码省略
  10.     }
复制代码
这个时候我们就来到AQS为我们提供的方法了,接下来小编一一讲解~~
  1.   public final void acquire(int arg) {
  2.         // 第一个调用了tryAcquire方法,这方法判断能不能拿到锁
  3.         // 强调,这里的tryAcquire的结果,最后是取反,最前面加了 !运算
  4.         if (!tryAcquire(arg) &&
  5.             // 后面的方法,慢慢道来,先保持神秘感
  6.             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  7.             selfInterrupt();
  8.     }
复制代码
4.3 tryAcquire方法

从方法名上看,字面意思就是尝试获取,获取什么呢?那当然是获取锁呀。
从acquire()点击tryAcquire方法进去看,AQS为我们提供了默认实现,默认如果没重写该方法,则抛出一个异常,这里就很突出模板方法模式这种设计模式的概念,提供了一个默认实现。
  1. protected boolean tryAcquire(int arg) {
  2.         throw new UnsupportedOperationException();
  3.     }
复制代码
同样,我们查看公平锁的实现  ~

最后来到了FairSync对象中的tyrAcquire方法了,重点来啦~~
  1. static final class FairSync extends Sync {
  2.         private static final long serialVersionUID = -3000897897090466540L;
  3.         final void lock() {
  4.             acquire(1);
  5.         }
  6.         // 尝试获取锁 拿到锁了返回:true,没拿到锁返回:false
  7.        protected final boolean tryAcquire(int acquires) {
  8.             // 获取当前线程
  9.             final Thread current = Thread.currentThread();
  10.             // 获取锁状态 , 自由状态 = 0,被上锁 = 1 ,> 1 表示重入
  11.             int c = getState();
  12.             // 判断当前状态是否等于自由状态
  13.             if (c == 0) {
  14.                 // hasQueuedPredecessors 判断自己需不需要排队,这个方法比较复杂,在下面补充部分详细解释,返回值,不需要排队返回false,然后取反,需要排队返回true
  15.                 if (!hasQueuedPredecessors() &&
  16.                    // compareAndSetState 如果不需要排队则直接进行CAS尝试加锁,成功则直接方法true
  17.                     compareAndSetState(0, acquires)) {
  18.                     // 成功获取锁,把当前线程设置成锁的拥有者,为了后续方便判断是不是可重入锁
  19.                     setExclusiveOwnerThread(current);
  20.                     return true;
  21.                 }
  22.             }
  23.             // 判断当前线程是否等于锁的持有线程,这里也证明了ReentrantLock是可重入锁
  24.             else if (current == getExclusiveOwnerThread()) {
  25.                 // 如果是重复锁,计数器 + 1
  26.                 int nextc = c + acquires;
  27.                 // 正常来说nextc不可能会小于0,于是判断如果小于0则直接抛出异常
  28.                 if (nextc < 0)
  29.                     throw new Error("Maximum lock count exceeded");
  30.                 // 赋值计数器+1的结果
  31.                 setState(nextc);
  32.                 // 如果重入成功返回true
  33.                 return true;
  34.             }
  35.              // 如果c不等于0,并且当前线程不等于持有锁的线程,直接返回false,因为就代表着有其他线程拿到锁了
  36.             return false;
  37.         }
  38.     }
复制代码
tryAcquire方法执行完成,又回到这里:tryAcquire方法拿到锁返回结果:true,没拿到锁返回:false。
一共分两种情况:

  • 第一种情况,拿到锁了,结果为true,通过取反,最后结果为false,由于这里是&&运算,后面的方法则不会进行,直接返回,代码正常执行,线程也不会进入阻塞状态。
  • 第二种情况,没有拿到锁,结果为false,通过取反,最后结果为true,这个时候,if判断会接着往下执行,执行这句代码:acquireQueued(addWaiter(Node.EXCLUSIVE), arg),先执行addWaiter方法。
  1.   public final void acquire(int arg) {
  2.         // tryAcquire执行完,回到这里
  3.         if (!tryAcquire(arg) &&
  4.             // Node.EXCLUSIVE 这里传进去的参数是为null,在Node类里面
  5.             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  6.             selfInterrupt();
  7.     }
复制代码
4.4 addWaiter方法

看到这里,还记得我们AQS里面有个head、tail,以及Node吧,如果印象模糊了,赶紧翻上去看看。
AQS在初始化的时候,数据大概是这个样子的,这个时候队列还没初始化的状态,所以head、tail都是为空。

addWaiiter这个方法总的来说做了什么事呢?
核心作用:把没有获取到的线程包装成Node节点,并且添加到队列中
具体逻辑分两步,判断队列尾部节点是不是为空,为空就去初始化队列,不为空就维护队列关系。
这里需要小伙伴掌握双向链表数据结构,才能更容易的明白怎么去维护一个队列的关系。
  1. private Node addWaiter(Node mode) {
  2.     // 因为在AQS队列里面,节点元素是Node,所以需要把当前类包装成一个node节点
  3.     Node node = new Node(Thread.currentThread(), mode);
  4.     // 把尾节点,赋值给pred,这里一共分两种两种情况
  5.     Node pred = tail;
  6.     // 判断尾部节点等不等于null,如果队列没有被初始化,tail肯定是个空
  7.     // 反而言之,如果队列被初始化了,head和tail都不会为空
  8.     if (pred != null) {
  9.          // 整个就是维护链表关系
  10.         // 把当前需要加入队列元素的上一个节点,指向队列尾部
  11.         node.prev = pred;
  12.         // CAS操作,如果队列的尾部节点是等于pred的话,就把tail 设置成 node,这个时候node就是最后一个节点了
  13.         if (compareAndSetTail(pred, node)) {
  14.             // 把之前尾部节点的next指向最后的的node节点
  15.             pred.next = node;
  16.             return node;
  17.         }
  18.     }
  19.     // 初始化队列
  20.     enq(node);
  21.     return node;
  22. }
复制代码
到这里小伙伴要记住:AQS队列默认是没有被初始化的,只有当发生竞争的时候,并且有线程没有拿到锁才会初始化队列,否则队列不会被初始化~
什么情况下不会被初始化呢?
1、线程没有发生竞争的情况下,队列不会被初始化,由tryAcquire方法就可以体现出,如果拿到锁了,就直接返回了。
2、线程交替执行的情况下,队列不会被初始化,交替执行的意思是,线程执行完代码后,释放锁,线程二来了,可以直接获取锁。这种就是交替执行,你用完了,正好就轮到我用了。
4.5 enq方法

这个方式就是为了初始化队列,参数是由addWaiter方法把当前线程包装成的Node节点。
  1. // 整个方法就是初始化队列,并且把node节点追加到队列尾部
  2. private Node enq(final Node node) {
  3.     // 进来就是个死循环,这里看代码得知,一共循环两次
  4.     for (;;) {
  5.         Node t = tail;
  6.         // 第一次进来tail等于null
  7.         // 第二次进来由于下面代码已经把tail赋值成一个为空的node节点,所以t现在不等于null了
  8.         if (t == null) {
  9.             // CAS把head设置成一个空的Node节点
  10.             if (compareAndSetHead(new Node()))
  11.                 // 把空的头节点赋值给tail节点
  12.                 tail = head;
  13.         } else {
  14.             // 第二次循环就走到这里,先把需要加入队列的上一个节点指向队列尾部
  15.             node.prev = t;
  16.             // CAS操作判断尾部是不是t如果是,则把node设置成队列尾部
  17.             if (compareAndSetTail(t, node))  {
  18.                 // 再把之前链表尾部的next属性,连接刚刚更换的node尾部节点
  19.                 t.next = node;
  20.                 return t;
  21.             }
  22.         }
  23.     }
  24. }
复制代码
通过enq代码我们可以得知一个很重要、很重要、很重要的知识点,在队列被初始化的时候,知道队列第一个元素是什么么?如果你认为是要等待线程的node节点,那么你就错了。
通过这两句代码得知,在队列初始化的时候,是new了一个空Node节点,赋值给了head,紧接着,又把head 赋值给tail。
  1. if (compareAndSetHead(new Node()))
  2.      // 把空的头节点赋值给tail节点
  3.    tail = head;
复制代码
初始化完成后,队列结构应该是这样子的。

队列初始化后,紧接着第二次循环对不对,t就是我们的尾部节点,node就是要被加入队列的node节点,也就是我们所谓要等待的线程的node节点,这里代码执行完后,直接return了,循环终止了。
  1. // 第二次循环就走到这里,先把需要加入队列的上一个节点指向队列尾部
  2. node.prev = t;
  3. // CAS操作判断尾部是不是t如果是,则把node设置成队列尾部
  4. if (compareAndSetTail(t, node))  {
  5.     // 再把之前链表尾部的next属性,连接刚刚更换的node尾部节点
  6.     t.next = node;
  7.     return t;
  8. }
复制代码
看了这幅图,哪怕对双向链表不熟悉,应该也可以看懂了吧, skr skr skr ~~~~

记住,这里队列初始化的时候,第一个元素是空,队列里面存在两个元素,切记切记切记,这也是面试需要注意的细节,把这个点勇敢的、大声的、自信的出说来,肯定能够证明你是看过源码的。
好了,最终addWaiter方法会返回一个初始化并且已经维护好,队列关系的Node节点出来。
  1.   public final void acquire(int arg) {
  2.         if (!tryAcquire(arg) &&
  3.             // addWaiter返回Node,紧接着调用acquireQueued 方法
  4.             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  5.             selfInterrupt();
  6.     }
复制代码
4.5 acquireQueued方法

看到这里,也就是我们lock方法的接近尾声了,我们拿到了队列中的数据,猜猜接下来需要做什么?
既然没拿到锁,就让线程进入阻塞状态,但是肯定不是直接就阻塞了,还需要经过一系列的操作,看源码:
  1. // node == 需要进队列的节点、arg = 1
  2. final boolean acquireQueued(final Node node, int arg) {
  3.     // 一个标记
  4.     boolean failed = true;
  5.     try {
  6.         // 一个标记
  7.         boolean interrupted = false;
  8.         // 又来一个死循环
  9.         for (;;) {
  10.             // 获取当前节点的上一个元素,要么就是为头部节点,要么就是其他排队等待的节点
  11.             final Node p = node.predecessor();
  12.             // 判断是不是头部节点,如果是头部节点则代表当前节点是排队在第一个,有资格去获取锁,也就是自旋获取锁
  13.             // 如果不是排队在第一个,前面的人还在排队后面的就更别说了去获取锁了
  14.             // 就比如食堂打饭,有人正在打饭,而排队在第一个人的可以人,可以去看看前面那个人打完了没
  15.             // 因为他前面没有没人排队,而后面的人就不同,前面的人还在排队,那么自己就只能老老实实排队了
  16.             if (p == head && tryAcquire(arg)) {
  17.                 // 如果能进入到这里,则代表前面那个打饭的人已经搞完了,可以轮第一个排队的人打饭了
  18.                 // 既然前面那个人打完饭了,就可以出队列了,会把thread、prev、next置空,等待GC回收
  19.                 setHead(node);
  20.                 p.next = null; // help GC
  21.                 failed = false;
  22.                 // 返回false,整个acquire方法返回false,就出去了
  23.                 return interrupted;
  24.             }
  25.             // 如果不是头部节点,就要过来等待排队了
  26.             // shouldParkAfterFailedAcquire 这方法会使当前循环再循环一次,相当于自旋一次获取锁
  27.             if (shouldParkAfterFailedAcquire(p, node) &&
  28.                 // 队列阻塞,整个线程就等待被唤醒了
  29.                 parkAndCheckInterrupt())
  30.                 interrupted = true;
  31.         }
  32.     } finally {
  33.         if (failed)
  34.             cancelAcquire(node);
  35.     }
  36. }
复制代码
看代码得知,如果当前传进来的节点的上一个节点,是等于head,那么又会调用tryAcquire方法,这里体现的就是自旋获取锁,为什么要这么做呢?
是为了避免进入阻塞的状态,假设线程一已经获取到锁了,然后线程二需要进入阻塞,但是由于线程二还在进入阻塞状态的路上,线程一就已经释放锁了。为了避免这种情况,第一个排队的线程,有必要在阻塞之前再次去尝试获取锁。
假设一:假设我们线程二在进入阻塞状态之前,尝试去获取锁,哎,竟然成功了,则会执行一下代码:
  1.   // 调用方法,代码在下面
  2.   setHead(node);
  3.   p.next = null; // help GC
  4.    private void setHead(Node node) {
  5.         head = node;
  6.         node.thread = null;
  7.         node.prev = null;
  8.     }
复制代码
如果拿到锁了,队列的内容,当然会发送变化,由图可见,我们会发现一个问题,队列的第一个节点,又是一个空节点。
因为当拿到锁之后,会把当前节点的内容,指针全部赋值为null,这也是个小细节哟。

假设二:如果当前节点的上一个节点,不是head,那么很遗憾,没有资格去尝试获取锁,那就走下面的代码。
在进入阻塞之前,会调用shouldParkAfterFailedAcquire方法,这个方法小编先告诉你,由于我们这里是死循环对吧,这个方法第一次调用会放回false,返回false则不会执行执行后续代码,再一次进入循环,经过一些列操作,还是没有资格获取锁,或者获取锁失败,则又会来到这里。
当第二次调用shouldParkAfterFailedAcquire方法,会放回ture,这个时候,线程才会调用parkAndCheckInterrupt方法,将线程进入阻塞状态,等待锁释放,然后被唤醒!!
  1. // 如果不是头部节点,就要过来等待排队了
  2. // shouldParkAfterFailedAcquire 这方法会使当前循环再循环一次,相当于自旋一次获取锁
  3. if (shouldParkAfterFailedAcquire(p, node) &&
  4.     // 队列阻塞,整个线程就等待被唤醒了
  5.     parkAndCheckInterrupt())
  6.     interrupted = true;
复制代码
4.5 parkAndCheckInterrupt方法
  1.   private final boolean parkAndCheckInterrupt() {
  2.         // 在这里被park,等待unpark,如果该线程被unpark,则继续从这里执行
  3.         LockSupport.park(this);
  4.         // 这个是获取该线程是否被中断过,这句代码需要结合lockInterruptibly方法来讲,小编就不详细说了,不然一篇文章讲太多了~~~~
  5.         return Thread.interrupted();
  6.     }
复制代码
到这里我们ReentrantLock整个加锁的过程,就相当于讲完啦,但是这才是最最最简单的一部分,因为还有很多场景没考虑到。
4.6 补充说明:shouldParkAfterFailedAcquire方法

上面说为什么这个方法第一次调用返回false,第二次调用返回ture,我们来看源码吧~~
这个方法主要做了一件事:把当前节点的,上一个节点的waitStatus状态,改为 - 1。
当线程进入阻塞之后,自己不会把自己的状态改为等待状态,而是由后一个节点进行修改。细节、细节、细节
举个例子:你躺在床上睡觉,然后睡着了,这个时候,你能告诉别人你睡着了吗?当然不行,因为你已经睡着了,呼噜声和打雷一样,怎么告诉别人。只有当后一个人来了,看到你在呼呼大睡,它才可以告诉别人你在睡觉。
  1. //pred 当前上一个节点,node 当前节点
  2. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  3.     // 第一次循环进来:获取上一个节点的线程状态,默认为0
  4.     // 第二次循环进来,这个状态就变成-1了,
  5.     int ws = pred.waitStatus;
  6.     // 判断是否等于-1,第一进来是0,并且会吧waitStatus状态改成-1,代码在else
  7.     // 第二次进来就是-1了,直接返回true,是当前线程进行阻塞
  8.     if (ws == Node.SIGNAL)
  9.         /*
  10.          * This node has already set status asking a release
  11.          * to signal it, so it can safely park.
  12.          */
  13.         return true;
  14.     // 判断是否大于0,waitStatus分几种状态,这里其他几种状态的源码就不一一讲了。
  15.     // = 1:由于在同步队列中等待的线程,等待超时或者被中断,需要从同步队列中取消等待,该节点进入该状态不会再变化
  16.     // = -1:后续节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后续节点,使后续节点继续运行
  17.     // = -2:节点在等待队列中,节点线程在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到同步状态中获取
  18.     // = -3:表示下一次共享式同步状态获取将会无条件地被传播下去
  19.     // = 0 :初始状态
  20.     if (ws > 0) {
  21.         /*
  22.          * Predecessor was cancelled. Skip over predecessors and
  23.          * indicate retry.
  24.          */
  25.         do {
  26.             node.prev = pred = pred.prev;
  27.         } while (pred.waitStatus > 0);
  28.         pred.next = node;
  29.     // 因为默认0,所以第一次会走到else方法里面
  30.     } else {
  31.         // CAS吧waitStatus修改成-1
  32.         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  33.     }
  34.     // 返回false,外层方法接着循环操作
  35.     return false;
  36. }
复制代码
五、ReentrantLock 解锁过程源码分析

5.1 unlock方法

讲完加锁过程,就来解锁过程吧,说实话,看源码这种经历,必须要自己花时间去看,去做笔记,去理解,大脑最好有个整体的思路,这样才会印象深刻。
  1. public class ReentrantLockTest {
  2.     public static void main(String[] args) {
  3.         // 创建公平锁
  4.         ReentrantLock lock = new ReentrantLock(true);
  5.         // 加锁
  6.         lock.lock();
  7.         hello();
  8.         // 解锁
  9.         lock.unlock();
  10.     }
  11.     public static void hello() {
  12.         System.out.println("Say Hello");
  13.     }
  14. }
复制代码
点击unlock解锁的方法,会调用到release方法,这个是AQS提供的模板方法,再来看tryRelease方法。
  1.   public void unlock() {
  2.         sync.release(1);
  3.     }
  4. public final boolean release(int arg) {
  5.     // tryRelease 释放锁,如果真正释放会把当前持有锁的线程赋值为空,否则只是计数器-1
  6.     if (tryRelease(arg)) {
  7.         Node h = head;
  8.         if (h != null && h.waitStatus != 0)
  9.             unparkSuccessor(h);
  10.         return true;
  11.     }
  12.     return false;
  13. }
复制代码
5.1 tryRelease方法

发现又是个抽象类,我们选择ReentrantLock类实现的

这里要注意:
1、当前解锁的线程,必须是持有锁的线程
2、state状态,必须是等于0,才算是真正的解锁,否则只是代表重入次数-1.
  1. protected final boolean tryRelease(int releases) {
  2.    // 获取锁计数器 - 1
  3.     int c = getState() - releases;
  4.     // 判断当前线程 是否等于 持有锁的线程,如果不是则抛出异常
  5.     if (Thread.currentThread() != getExclusiveOwnerThread())
  6.         throw new IllegalMonitorStateException();
  7.     // 返回标志
  8.     boolean free = false;
  9.     // 如果计算器等于0,则代表需要真正释放锁,否则是代表重入次数-1
  10.     if (c == 0) {
  11.         free = true;
  12.         // 将持有锁的线程赋值空
  13.         setExclusiveOwnerThread(null);
  14.     }
  15.     // 重新设置state状态
  16.     setState(c);
  17.     return free;
  18. }
复制代码
执行完tryRelease方法,返回到release,进行if判断,如果返回false,就直接返回了,否则进行解锁操作。
  1. public final boolean release(int arg) {
  2.     // tryRelease方法返回true,则表示真的需要释放锁
  3.     if (tryRelease(arg)) {
  4.         // 如果是需要真正释放锁,先获取head节点
  5.         Node h = head;
  6.         // 第一种情况,假设队列没有被初始化,这个时候head是为空的,则不需要进行锁唤醒
  7.         // 第二种情况,队列被初始化了head不为空,并且只要有线程在队列中排队,waitStatus在被加入队列之前,会把当前节点的上一个节点的waitStatus改为-1
  8.         // 所以只有满足h != null && h.waitStatus != 0 这个条件表达式,才能真正代表有线程正在排队
  9.         if (h != null && h.waitStatus != 0)
  10.             // 解锁操作,传入头节点信息
  11.             unparkSuccessor(h);
  12.         return true;
  13.     }
  14.     return false;
  15. }
复制代码
5.2 unparkSuccessor方法

这里的参数传进来的是head的node节点信息,真正解锁的线程是head.next节点,然后调用unpark进行解锁。
[code]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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

金歌

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表