【多图预警】带你了解ReentrantLock底层执行原理、揭开AQS的神秘面纱

发布时间:2022-03-01 11:04:12 作者:yexindonglai@163.com 阅读(684)

什么是AQS

AQS全名为AbstractQueuedSynchronizer ,是JDK1.5之后并发包java.util.concurrent(简称JUC)里面的一个抽象类类,这是一个在并发编程很常用的工具类,看名字就知道,这是一个队列,并且是线程安全的队列,比较特别的是,在操作数据的时候,是使用CAS(Compare And Swap)来保证原子性的,而不是大家熟知的synchronized;使用这个AQS可以实现ReentrantLock、CountDownLatch(倒计时门栓)、Semaphore(信号量)、ReentrantReadWriteLock(读写锁)等一些列的并发辅助工具;
<br>
我们看看它是如何实现这些工具类的,很显然,它们都在里面维护了一个内部抽象类Sync,由Sync实现AQS

ReentrantLock 【独占锁】

在这里插入图片描述

CountDownLatch 【倒计时门栓】

在这里插入图片描述

Semaphore 【信号量】

在这里插入图片描述

ReentrantReadWriteLock 【读写锁】

在这里插入图片描述

AQS 的内部结构

AQS常用方法

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式(读写锁会用到)。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式(读写锁会用到)。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

如果你有兴趣,愿意折腾,那你完全可以继承AQS然后重写以上的方法来实现自己的锁机制;

·staste 锁标志位

AQS 内部维护了一个state 字段,主要用来标志是否已上锁,默认值是0,表示未上锁,一旦有线程获得锁,则会将state 字段置为1,在ReentrantLock中,当有重入锁重复获得锁时会在原来的基础上在 加1,解锁则进行减 1,减至 0时代表锁为空闲状态,即没有线程获取锁,是自由状态的;并且该字段被volatile关键字修饰,在各个线程中保证了可见性以及防止指令重排序;

  1. /**
  2. * The synchronization state.
  3. */
  4. private volatile int state;

在这里插入图片描述

·CLH队列

在AQS内部还维护了一个CLH队列,在源码中是这样介绍的:

The wait queue is a variant of a “CLH” (Craig, Landin, and

  1. * Hagersten) lock queue. CLH locks are normally used for
  2. * spinlocks.

意思是这个队列由 Craig,、Landin 和 Hagersten 三个人发明,所以取每个人名字的头字母组成,最后一句话的意思是说:这个CLH队列通常用来做自旋操作;源码里面还画出了一个粗略的队列图
在这里插入图片描述
但其实,这个CLH要比源码图上复杂的多,这个队列是一个<font color="red">双向链表,也是一个双端队列</font>,

  • 双向队列 : 除了元素本身外,还维护了2个指针,一个(prev)指向上一个元素,另一个(next)指向下一个元素
  • 双端队列:单端队列只有头部,如果你要找某个元素的话,就必须从头部开始,一个个节点开始往下找;双端队列既有头也有尾,你可以选择从头部开始往下找元素,也可以从尾部开始往上找元素;
    在这里插入图片描述

在源码中,不管队头还是队尾,都是一个Node节点

  1. /**
  2. * Head of the wait queue, lazily initialized. Except for
  3. * initialization, it is modified only via method setHead. Note:
  4. * If head exists, its waitStatus is guaranteed not to be
  5. * CANCELLED.
  6. */
  7. private transient volatile Node head;
  8. /**
  9. * Tail of the wait queue, lazily initialized. Modified only via
  10. * method enq to add new wait node.
  11. */
  12. private transient volatile Node tail;

在来看看Node节点这个类里面都有哪些玩意,以及这些玩意都是干啥的~

  1. static final class Node {
  2. static final AbstractQueuedSynchronizer.Node SHARED = new AbstractQueuedSynchronizer.Node();
  3. static final AbstractQueuedSynchronizer.Node EXCLUSIVE = null;
  4. static final int CANCELLED = 1;
  5. static final int SIGNAL = -1;
  6. static final int CONDITION = -2;
  7. static final int PROPAGATE = -3;
  8. volatile int waitStatus;
  9. volatile AbstractQueuedSynchronizer.Node prev;
  10. volatile AbstractQueuedSynchronizer.Node next;
  11. volatile Thread thread;
  12. AbstractQueuedSynchronizer.Node nextWaiter;
  13. }

依次介绍下Node节点中的属性都代表什么意思,有什么作用

  • SHARED : 共享模式节点
  • EXCLUSIVE:独占模式节点
  • prev: 前置节点,这是一个指针,指向队列中上一个元素
  • next:后继节点,这也是一个指针,指向队列中的下一个元素
  • thread :每个线程在进入队列时都会封装成一个Node节点,而thread变量就是用来存储这个线程的指针;
  • waitStatus : 等待状态,下面说明

    以下是waitStatus的状态值

  • <font color="red">CANCELLED</font>:(waitStatus的状态)由于超时或中断,此节点被取消。节点一旦被取消了就不会再改变状态。特别是,取消节点的线程不会再阻塞。一直循环获取锁
  • <font color="red">SIGNAL</font> :表示这个节点已挂起在等待唤醒。,就是告诉上一个节点,执行完时间片后通知一下自己;此节点后面的节点已(或即将)被阻止(通过park),因此当前节点在释放或取消时必须断开后面的节点,为了避免竞争,acquire方法时前面的节点必须是SIGNAL状态,然后重试原子acquire,最后在失败时调用patk()方法阻塞
  • <font color="red">CONDITION </font>:等待条件状态,在等待队列中, 此节点当前在条件队列中。标记为CONDITION的节点会被移动到一个特殊的条件等待队列(此时状态将设置为0),直到条件时才会被重新移动到同步等待队列 。(此处使用此值与字段的其他用途无关,但简化了机制。)
  • <font color="red">PROPAGATE</font>:传播, 状态需要向后传播,应将releaseShared传播到其他节点。这是在doReleaseShared中设置的(仅适用于头部节点),以确保传播继续,即使此后有其他操作介入。

CLH队列的真正面目

通过以上的介绍,如果要准确地描述出CLH队列,那么,AQS的CLH队列应该是下图的样子,其中T1T2T3 就是thread变量指向每个不同的线程
在这里插入图片描述

通过 ReentrantLock 解读AQS

我们都知道 ReentrantLock 是一个独占锁,那么当我们调用lock() 方法方法的时候,它里面都走了哪些事情呢?公平锁和非公平锁的区别又在哪里呢?其实还要分上锁的次数,首次上锁、第二次上锁、第三次上锁走的流程都不一样,那么接下来我们就来由浅入深地慢慢揭开它神秘的面纱!
<br>

温馨提示

<font color="red">流程图中都会显示使用到哪些方法,每一个方法在后面会有解释,所以大家看文章的时候不要着急,除了文字之外,我都会尽量用流程图来说明清除</font>
<br>

非公平锁

非公平锁就像它的字面意思那样,不公平的嘛,在这个模式下,每个线程就像是强盗一样,同时去抢一把锁,谁抢到就是谁的。官方的解释是每个线程以抢占的方式进行获取锁资源,多个线程同时争抢的情况下只会有一个线程获得锁,当获取锁的线程执行完业务代码后会释放锁,并且线程会引起下一轮争抢,依次循环往复;直到队列 中的线程都执行完为止;

在这里插入图片描述
接下来我们运行以下代码,看看它上锁的过程中都经过了哪些流程

  1. package com.Lock;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. public class LockTest extends Thread{
  4. private ReentrantLock lock = null;
  5. // 延时的时间,单位:ms
  6. private long time ;
  7. // 构造方法
  8. public LockTest(ReentrantLock lock,long time) {
  9. this.time = time;
  10. this.lock = lock;
  11. }
  12. @Override
  13. public void run() {
  14. lock.lock(); // 上锁
  15. try {
  16. // 开始延时,延时代表着在执行业务逻辑所花费的时间
  17. if(time > 0) Thread.sleep(time);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. lock.unlock(); // 解锁
  22. }
  23. }
  24. class MainClass{
  25. public static void main(String[] args) {
  26. // 实例化锁,构造函数的参数,默认为true:独占锁,
  27. ReentrantLock lock = new ReentrantLock();
  28. // 创建3个线程来争抢锁
  29. //第一个线程,进入后延时2000秒,方便我们debug
  30. LockTest lockTest = new LockTest(lock,2000000);
  31. lockTest.setName("线程AA");
  32. lockTest.start();
  33. // 因为第一个线程独占,所以第二个会进入队列
  34. LockTest lockTest1 = new LockTest(lock,0);
  35. lockTest1.setName("线程BB");
  36. lockTest1.start();
  37. // 第三个线程也会进入队列
  38. LockTest lockTest2 = new LockTest(lock,0);
  39. lockTest2.setName("线程cc");
  40. lockTest2.start();
  41. }
  42. }

非公平锁—第一个线程上锁过程(首次上锁)

首先,第一次上锁的时候跟队列没有任何关系,因为你第一个线程是没有人跟你抢的,轻而易举的就拿到了这把锁,然后执行业务代码,需要注意的是,此时CLH队列还没有形成,因为只有一个线程的情况下不需要队列;
在这里插入图片描述

非公平锁—第二个线程抢锁

为什么第一次就叫上锁,第二次就叫抢锁呢?因为啊,第一个线程每人跟它抢,直接拿就行;粗略地看,第二次抢锁过程是这样的
在这里插入图片描述

非公平锁会进行2次抢锁,可以说是非常简单粗暴,一上来先抢一次,抢不到在判断锁资源是否空闲,如果空闲的话在抢一次,最后抢不到才会进入队列并阻塞当前线程;使其不在自旋;注意,严格意义上说ReentrantLock不是自旋锁,它属于<font color="red">自适应自旋</font> ,不会无限自旋下去,因为自旋也是需要消耗CPU资源的,
上面的图只是一个大概流程图,因为我要由浅入深嘛,如果一下子讲的太深了,会让人觉得很复杂,接下来我们看看详细的流程
在这里插入图片描述

说明

·<font color="red"> 粉红色部分表示抢锁失败的流程走向</font>
·<font color="green"> 绿色部分表示抢锁成功的流程走向</font>
·<font color="#DDDD22"> 黄色部分表示注释的一些细节</font>
·<font color="violet"> 紫色部分表示正在使用CAS尝试上锁</font>
看到这张图有没有菊花一紧的感觉? 没错,确实很复杂,这个流程图是我一点点手动debug后画出来的;里面牵扯到的东西很绕,特别是循环这一块;当然我们只需要记住上面那个简化的流程图就好,关于细节方面的东西,你必须自己去调试过才能知道它里面流程的具体走向;但第二个线程进行抢锁失败后,会进入队列,此时,队列结构如下
在这里插入图片描述
有细心的童鞋会发现,这里怎么会有个空节点呢? 其实这个节点是用来占位的,给谁占位的呢?当然是给T1线程占位的啦,因为我们刚刚的线程延时了两千秒啊,足足半个小多小时呢,T1线程还在执行中,这个空节点就是T1的;
<br>
那么这时候问题又来了,它为什么要弄个空节点来占位啊? 直接把空节点删掉,把T2放进head节点里面不就行了吗?

答:其实是因为我们要用到节点里面的waitStatus属性,就是等待状态,这个状态决定了我们要不要唤醒下一个线程,在独占模式下,这个状态只会是0或者-1,所以不管怎么样,它都会唤醒队列中的下一个线程;感兴趣的童鞋可以回到上面介绍waitStatus属性值在看一遍,这里不过多介绍;

非公平锁—第三个线程抢锁

第三次抢锁时可参照上面的比较流程图做推理,主要区别就是初始化队列那一块,

  • 第二个线程抢锁时,因为队列还没初始化,所以抢锁失败时会先去初始化这个队列,然后在把头尾节点指向对应的内存空间,
  • 第三个线程抢锁时,因为队列已经创建好了,所以直接在队列尾部插入第三个线程的节点;

所以当第三个线程进入队列后,结构是这样的
在这里插入图片描述
非公平锁的加锁过程就已经讲完了,因为公平锁和非公平锁的解锁过程是一样的,所以解锁这一块单独放到最后面去介绍;接下来我们先看看公平锁的加锁流程

公平锁

公平锁其实很简单嘛,就像去银行办业务一样,每个人必须先拿号,然后在人群中排队,一直等叫到自己号的时候,就可以进去窗口办理业务了;
在这里插入图片描述
运行以下的代码

  1. package com.Lock;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. public class LockTest extends Thread{
  4. private ReentrantLock lock = null;
  5. // 延时的时间,单位:ms
  6. private long time ;
  7. // 构造方法
  8. public LockTest(ReentrantLock lock,long time) {
  9. this.time = time;
  10. this.lock = lock;
  11. }
  12. @Override
  13. public void run() {
  14. lock.lock(); // 上锁
  15. try {
  16. // 开始延时,延时代表着在执行业务逻辑所花费的时间
  17. if(time > 0) Thread.sleep(time);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. lock.unlock(); // 解锁
  22. }
  23. }
  24. class MainClass{
  25. public static void main(String[] args) {
  26. // 实例化锁,构造函数的参数,默认为true:独占锁,,这里我们换成false:公平锁
  27. ReentrantLock lock = new ReentrantLock(false);
  28. // 创建3个线程来争抢锁
  29. //第一个线程,进入后延时2000秒,方便我们debug
  30. LockTest lockTest = new LockTest(lock,2000000);
  31. lockTest.setName("线程AA");
  32. lockTest.start();
  33. // 因为第一个线程独占,所以第二个会进入队列
  34. LockTest lockTest1 = new LockTest(lock,0);
  35. lockTest1.setName("线程BB");
  36. lockTest1.start();
  37. // 第三个线程也会进入队列
  38. LockTest lockTest2 = new LockTest(lock,0);
  39. lockTest2.setName("线程cc");
  40. lockTest2.start();
  41. }
  42. }

公平锁—第一次上锁

和非公平锁一样,第一次上锁时跟队列没什么关系,但是公平锁和非公平锁的第一次上锁还是有区别的,具体哪些区别我们先看流程图
在这里插入图片描述

通过流程图可以看到,公平锁在上锁前会先去队列里面看看有没有节点,因为公平锁是公平的,队列里面的节点要比当前线程优先,所以它必须要先把队列里面的线程执行完才能执行当前线程;因为现在是第一次上锁,队列里面肯定是没有其他线程,而且队列都还没创建,所以这个线程肯定能拿到锁;

公平锁—第二次上锁

以后不管是第二次上锁还是第三次、第四次还是第N次,除非锁已被释放,不然所有的线程都会在后面排队,因为它必须符合公平的概念:先到先得在这里插入图片描述

解锁

在解锁的层面,公平锁和非公平锁是一样的,因为队列里面就已经保证了顺序,所以,在解锁的时候,除了释放锁之外,还要将队列中的线程唤醒,当然了,唤醒也是按顺序来的,有人就会说了,不对啊,你公平锁按顺序来唤醒没问题,但是非公平锁也是这么干的吗?我想告诉你,还真是这么干的,因为非公平锁,在上锁的时候就已经抢过锁资源了,已经达到了非公平的概念,所以,这边唤醒的时候就是公平的了!接下来我们看看解锁流程
在这里插入图片描述
相对于加锁,解锁要简单地多,流程也不复杂,代码的可读性也高,上面这张图,是我经过精细调整过的,只要你认识字,就一定能看懂;

源码解析

非公平锁的 lock() 方法

  1. /**
  2. * Performs lock. Try immediate barge, backing up to normal
  3. * acquire on failure.
  4. */
  5. final void lock() {
  6. if (compareAndSetState(0, 1))
  7. setExclusiveOwnerThread(Thread.currentThread());
  8. else
  9. acquire(1);
  10. }

非公平的简单粗暴就是在这里,一上来不管三七二十一,先抢一次,抢到了,直接执行,抢不到在执行acquire(1)方法;
compareAndSetState(0, 1) 表示使用cas抢锁,内部逻辑是判断state 是否为0,如果是0的话,就将其改为1 ,state =0表示锁是空闲状态,state =1 表示锁已被线程独占;能进入到setExclusiveOwnerThread(Thread.currentThread());方法就表示cas已经抢到锁了, 接下来就是设置标志位,方法内容如下

  1. protected final void setExclusiveOwnerThread(Thread thread) {
  2. exclusiveOwnerThread = thread;
  3. }

可以看到里面非常简单, 就是设置一下属性,将获取锁的线程指向当前线程,
综上所述,lock()方法的流程走向如下
在这里插入图片描述

公共 acquire(int arg) 方法

  1. public final void acquire(int arg) {
  2. if (!tryAcquire(arg) &&
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  4. selfInterrupt();
  5. }

acquire() 方法是AQS的方法, 在if判断语句里面,它先调用了tryAcquire(arg) 方法, 这个方法是用来抢锁的,但是在非公平锁里面,它不是一定会抢,这得看条件,里面的方法我们一会在说;需要注意的,可以看到tryAcquire(arg) 方法的前面有个感叹号!,这意思是说,如果抢不到的话,我就执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,这个方法主要是做入队和阻塞的,参数中还有个addWaiter方法,我们上面讲到的空节点,就是在这个方法里面创建的;下面会细讲里面的流程,综上所述,acquire() 方法里面的流程走向如下
在这里插入图片描述

非公平锁 tryAcquire() 方法

  1. protected final boolean tryAcquire(int acquires) {
  2. return nonfairTryAcquire(acquires);
  3. }

tryAcquire()中间做了一层转换,其实调用的是nonfairTryAcquire()方法

  1. final boolean nonfairTryAcquire(int acquires) {
  2. final Thread current = Thread.currentThread();
  3. int c = getState();
  4. if (c == 0) {
  5. if (compareAndSetState(0, acquires)) {
  6. setExclusiveOwnerThread(current);
  7. return true;
  8. }
  9. }
  10. else if (current == getExclusiveOwnerThread()) {
  11. int nextc = c + acquires;
  12. if (nextc < 0) // overflow
  13. throw new Error("Maximum lock count exceeded");
  14. setState(nextc);
  15. return true;
  16. }
  17. return false;
  18. }

进入方法后,先去执行 c = getState()来获取state的状态,刚刚我们讲到,0表示空闲,1表示以上锁;如果是空闲状态的话,就在抢一次锁,compareAndSetState(0, acquires) 方法就是用来上锁的,抢不到则判断当前线程是否获得独占锁的线程,如果是,就代表着锁正在重入,重入锁的逻辑就在这里面判断的,重入锁也叫递归锁,感兴趣的可以看我另外一篇文章:原来java有这么多把锁,图解java中的17把锁 ,里面有详细的介绍;综上所述,tryAcquire()方法流程走向如下
在这里插入图片描述

addWaiter(Node mode)方法

  1. /**
  2. * Creates and enqueues node for current thread and given mode.
  3. *
  4. * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
  5. * @return the new node
  6. */
  7. private Node addWaiter(Node mode) {
  8. Node node = new Node(Thread.currentThread(), mode);
  9. // Try the fast path of enq; backup to full enq on failure
  10. Node pred = tail;
  11. if (pred != null) {
  12. node.prev = pred;
  13. if (compareAndSetTail(pred, node)) {
  14. pred.next = node;
  15. return node;
  16. }
  17. }
  18. enq(node);
  19. return node;
  20. }

此方法也是AQS的方法,这个主要的作用就是在队尾追加节点,做的都是一些变量指向工作,先判断tall队尾是否存在,如果存在则将当前节点追加到队尾;没啥好说的,最重要的是enq方法。下一步就做这个方法的解析
在这里插入图片描述

enq(node)方法

  1. /**
  2. * Inserts node into queue, initializing if necessary. See picture above.
  3. * @param node the node to insert
  4. * @return node's predecessor
  5. */
  6. private Node enq(final Node node) {
  7. for (;;) {
  8. Node t = tail;
  9. if (t == null) { // Must initialize
  10. if (compareAndSetHead(new Node()))
  11. tail = head;
  12. } else {
  13. node.prev = t;
  14. if (compareAndSetTail(t, node)) {
  15. t.next = node;
  16. return t;
  17. }
  18. }
  19. }
  20. }

进入enq()后直接就是一个循环, 但是大家不要被它的for (;;)给蒙蔽了, 虽然看起来像个死循环,但是循环最多只会循环2次;第一次循环执行时,会先判断队列是否存在,但是它判断的是tail(队尾)是否为空,这是因为队头和队尾是相对应的,队头为空的话,队尾一定是空的,相反,如果队头有值,队尾也一定有值,所以第一个if里面,如果队列为空,就是把队头给创建出来,这里创建的就是刚刚说的空节点,仔细看这行代码compareAndSetHead(new Node()),直接new 一个Node节点,但是不做其他的任何处理,只是一个单纯的空节点,刚刚说过了,这个空节点是给当前获得锁的线程占位的;tail = head 队头和队尾都指向同一块内存,它们俩都是空节点,这时候队列的雏形就形成了;接着往下一个循环,第二次循环执行就是else里面的内容了,因为刚刚第一次已经把队列创建出来了,最关键的就是compareAndSetTail(t, node)这行代码,它将当前线程塞到队尾,队头还是空节点,执行完就退出循环,此时队列的结构如下
在这里插入图片描述
综上所述,enq()方法的执行流程走向如下
在这里插入图片描述

acquireQueued(final Node node, int arg)方法

  1. /**
  2. * Acquires in exclusive uninterruptible mode for thread already in
  3. * queue. Used by condition wait methods as well as acquire.
  4. *
  5. * @param node the node
  6. * @param arg the acquire argument
  7. * @return {@code true} if interrupted while waiting
  8. */
  9. final boolean acquireQueued(final Node node, int arg) {
  10. boolean failed = true;
  11. try {
  12. boolean interrupted = false;
  13. for (;;) {
  14. final Node p = node.predecessor();
  15. if (p == head && tryAcquire(arg)) {
  16. setHead(node);
  17. p.next = null; // help GC
  18. failed = false;
  19. return interrupted;
  20. }
  21. if (shouldParkAfterFailedAcquire(p, node) &&
  22. parkAndCheckInterrupt())
  23. interrupted = true;
  24. }
  25. } finally {
  26. if (failed)
  27. cancelAcquire(node);
  28. }
  29. }

映入眼帘的还是for (;;)循环,这个方法主要做了2件事,第一件是在抢一次锁,第二件是阻塞当前线程,

  • 先看第一个,也就是第一个if语句的内容,它先判断当前节点的前置节点是否是队头,如果是队头就在抢一次锁,那什么情况下当前节点的前置是队头呢?答案是第二个线程入队的时候,因为你现在队列里只有2个节点,要么是头部,要么是尾部,所以第二个节点的前置节点肯定是头部;
    在这里插入图片描述
    我们主要看红色框内的代码,当线程抢到锁资源后就会执行红框内的代码,我们一个个地讲解,setHead(node) 表示将当前线程设为头部,因为头部一定是获得锁的线程,再看下一行p.next = null; // help GC 在把原来队头节点置空,这样做是为了方便垃圾回收机制回收内存空间(GC);
  • 第二件事,能执行到第二个if,就代表着线程没抢到锁,没抢到锁的情况下,因为这是个死循环,你不能一直循环啊,要是让cpui飙要00%可咋整?所以要让当前线程睡一会shouldParkAfterFailedAcquire(p, node) 方法主要是判断waitStatus 的状态, 在ReentrantLock里面都会返回true;因为ReentrantLock是独占模式,waitStatus` 的值只有0和-1,不管0还是-1,返回的都是true,这里不过多赘述,以下是翻译了代码的注释;
    1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    2. int ws = pred.waitStatus;
    3. //拿到前驱的状态
    4. if (ws == Node.SIGNAL)
    5. //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
    6. return true;
    7. if (ws > 0) {
    8. /* * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
    9. * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)! */
    10. do {
    11. node.prev = pred = pred.prev;
    12. } while (pred.waitStatus > 0);
    13. pred.next = node;
    14. } else {
    15. // 如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
    16. compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    17. }
    18. return false;
    19. }
    调用parkAndCheckInterrupt() 方法后就会进入阻塞了,虽然循环还没结束;下次唤醒时还会继续执行循环的内容;
    1. private final boolean parkAndCheckInterrupt() {
    2. LockSupport.park(this);
    3. return Thread.interrupted();
    4. }
    综上所述,acquireQueued() 方法的流程走向如下
    在这里插入图片描述

公平锁的tryAcquire() 方法

  1. protected final boolean tryAcquire(int acquires) {
  2. final Thread current = Thread.currentThread();
  3. int c = getState();
  4. if (c == 0) {
  5. if (!hasQueuedPredecessors() &&
  6. compareAndSetState(0, acquires)) {
  7. setExclusiveOwnerThread(current);
  8. return true;
  9. }
  10. }
  11. else if (current == getExclusiveOwnerThread()) {
  12. int nextc = c + acquires;
  13. if (nextc < 0)
  14. throw new Error("Maximum lock count exceeded");
  15. setState(nextc);
  16. return true;
  17. }
  18. return false;
  19. }

比起非公平锁,公平锁的tryAcquire()方法就显得友好很多,它会先去判断队列是否已生成,因为要公平嘛,如果队列已存在的话,就乖乖在队列里面排队,如果不存在就直接拿锁;
这个方法里面公平锁和非公平锁最大区别就是!hasQueuedPredecessors(),其他的代码都是一样的,

  • 公平锁先入队
  • 非公平锁先抢锁在入队
    在这里插入图片描述
    通过了解后,我们就知道公平锁的tryAcquire()方法执行流程如下
    在这里插入图片描述

公平锁 hasQueuedPredecessors()方法

  1. public final boolean hasQueuedPredecessors() {
  2. // The correctness of this depends on head being initialized
  3. // before tail and on head.next being accurate if the current
  4. // thread is first in queue.
  5. Node t = tail; // Read fields in reverse initialization order
  6. Node h = head;
  7. Node s;
  8. return h != t &&
  9. ((s = h.next) == null || s.thread != Thread.currentThread());
  10. }

这个就一个作用,判断队列是否存在,

  • 首先看if语句里面的第一个表达式h != th是队头,t是队尾,一旦队列生成,队头和队尾永远不会相等,只有队列还未生成的时候才会相等,因为队列还未生成时,队头和队尾都是null值,null == null的结果为true
  • 在看第二个表达式(s = h.next) == null ,它先将队头的后继 节点赋给s,就是判断队头有没有下一个节点
  • 第三个表达式s.thread != Thread.currentThread() ,判断队头的后继节点是否当前线程,

这时候有的童鞋就要问了,判断队列是否为空不就行了,干嘛要这么多判断? 你想想啊,我们用的是多线程,是在并发的情况下判断的,如果是单个线程,直接判断head是否为空就可以了,但是要为了数据的原子性,所以以上三个表达式的判断合并起来就是为了保证数据的原子性的;

解锁 unLock()

  1. public void unlock() {
  2. sync.release(1);
  3. }

当我们调用解锁方法,实际上是调用了syncrelease()方法

  1. public final boolean release(int arg) {
  2. if (tryRelease(arg)) {
  3. Node h = head;
  4. if (h != null && h.waitStatus != 0)
  5. unparkSuccessor(h);
  6. return true;
  7. }
  8. return false;
  9. }

方法也很简单,直接进行解锁,解锁后判断队列是否存在,然后唤醒队列中的线程;但是这里需要注意一下,如果在没上锁的情况下解锁,会抛出IllegalMonitorStateException异常,tryRelease()方法中有这一层判断

  1. protected final boolean tryRelease(int releases) {
  2. int c = getState() - releases;
  3. if (Thread.currentThread() != getExclusiveOwnerThread())
  4. throw new IllegalMonitorStateException();
  5. boolean free = false;
  6. if (c == 0) {
  7. free = true;
  8. setExclusiveOwnerThread(null);
  9. }
  10. setState(c);
  11. return free;
  12. }

解锁流程如下
在这里插入图片描述

unparkSuccessor(Node node)方法

  1. private void unparkSuccessor(Node node) {
  2. /*
  3. * If status is negative (i.e., possibly needing signal) try
  4. * to clear in anticipation of signalling. It is OK if this
  5. * fails or if status is changed by waiting thread.
  6. */
  7. int ws = node.waitStatus;
  8. if (ws < 0)
  9. compareAndSetWaitStatus(node, ws, 0);
  10. /*
  11. * Thread to unpark is held in successor, which is normally
  12. * just the next node. But if cancelled or apparently null,
  13. * traverse backwards from tail to find the actual
  14. * non-cancelled successor.
  15. */
  16. Node s = node.next;
  17. if (s == null || s.waitStatus > 0) {
  18. s = null;
  19. for (Node t = tail; t != null && t != node; t = t.prev)
  20. if (t.waitStatus <= 0)
  21. s = t;
  22. }
  23. if (s != null)
  24. LockSupport.unpark(s.thread);
  25. }

这是一个唤醒队列中线程的方法,通常情况下会唤醒队列中的的下一个线程,但是如果队列中下一个线程的waitStatus > 0的话,就会从队尾开始往上遍历拿线程;
在这里插入图片描述

看完如果还还不懂的话,那。。。。那就评论区讨论吧,在下一定知无不言!

关键字Java