文章目录
- 1. 可重入锁
- 1.1. 概述
- 1.2. 可重入锁类型
- 1.3. Synchronized 可重入实现机理
- 2. LockSupport
- 2.1. LockSupport 是什么
- 2.2. 3种线程等待唤醒的方法
- 2.2.1 Object 的等待与唤醒
- 2.2.2. Condition接口中的等待与唤醒
- 2.2.3. 传统的 synchronized 和 Lock 实现等待唤醒通知的约束
- 2.3. LockSupport 类
- 2.3.1 阻塞
- 2.3.2 唤醒
- 2.3.3 LockSupport 代码示例
- 2.3.4 LockSupport 重点说明
- 2.4 LockSupport 面试题
- 2.4.1 为什么可以先唤醒线程后阻塞线程?
- 2.4.2 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
- 3. AQS
- 3.1 AQS 前置知识
- 3.2 AQS 是什么?
- 3.3 AQS 是 JUC 的基石
- 3.4 AQS 能干嘛
- 3.5 AQS 初步认识
- 3.5.1 AQS初识
- 3.5.2 AQS内部体系架构
- 3.5.3. AQS同步队列的基本结构
- 3.5.4 AQS底层是怎么排队的?
- 3.6 从 ReentrantLock 开始解读 AQS
- 3.6.1 ReentrantLock 实现关系
- 3.6.2 公平锁 & 非公平锁
- 3.7 AQS 源码解读
- 3.7.1 从非公平锁的 lock() 入手
- 3.7.2 acquire()
- 3.7.3 tryAcquire(arg) :尝试抢占 arg 个线程
- 3.7.4 addWaiter(Node.EXCLUSIVE) :进入等候区
- 3.7.5 acquireQueued(node, arg)
- 3.7.6 unlock()
- 3.7、AQS 总结
- 3.7.1 AQS 的考点
- 3.7.2 AQS 源码解读案例图示
1. 可重入锁
1.1. 概述
- 可重入锁,又叫递归锁
- 同一个线程,在外层方法获取锁的时候,再进入该线程内层方法会自动获取锁(前提,锁对象必须是同一个对象),不会因为之前获取还没释放而阻塞。
- Java 中的 ReentrantLock 和 Synchronized 都是可重入锁
- 可重入锁的优点就是避免死锁
1.2. 可重入锁类型
- 隐式锁,Synchronized 关键字使用的锁,默认是可重入锁,由 JVM 层面自动控制加解锁
- 显式锁,即 Lock
1.3. Synchronized 可重入实现机理
- 每一个锁对象都有一个锁计数器和一个指向持有该线程的指针
- 当执行 monitorenter 时
- 若目标锁对象的计数器为0,那么说明没有其他线程持有它,那么 Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器 +1
- 若目标对象的计数器不为0
- 若该锁的持有线程是当前线程,则将计数器 +1
- 否则等待,直至持有该锁的线程将其释放,也即直至计数器为0
- 当执行 monitorexit 时,虚拟机会将该锁对象的计数器 -1,计数器为0时表示释放该锁
2. LockSupport
2.1. LockSupport 是什么
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
- LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程,可以将其看作是线程等待唤醒机制(wait/notify)的加强版
2.2. 3种线程等待唤醒的方法
- 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
- 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
- LockSupport类可以阻塞 park() 当前线程以及唤醒 unpark() 指定被阻塞的线程
2.2.1 Object 的等待与唤醒
-
正常的使用情况,及其结果
static Object objectLock = new Object(); private static void synchronizedWaitNotify() { new Thread(() -> { synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { objectLock.wait(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } }, "A").start(); new Thread(() -> { synchronized (objectLock) { objectLock.notify(); // 唤醒 System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } }, "B").start(); }
- 程序运行结果:A 线程先执行,执行 objectLock.wait() 后被阻塞,B 线程在 A 线程之后执行 objectLock.notify() 将 A线程唤醒
-
异常情况 1 : wait() 和 notify() 方法 不在 Synchronized 内使用
static Object objectLock = new Object(); private static void synchronizedWaitNotify() { new Thread(() -> { //synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { objectLock.wait(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); //} }, "A").start(); new Thread(() -> { //synchronized (objectLock) { objectLock.notify(); // 唤醒 System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); //} }, "B").start(); }
- 不在 synchronized 关键字中使用 wait() 和 notify() 方法 ,将抛出java.lang.IllegalMonitorStateException 异常
-
异常情况 2:先 notify() 后 wait()
static Object objectLock = new Object(); private static void synchronizedWaitNotify() { new Thread(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { objectLock.wait(); // 等待 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } }, "A").start(); new Thread(() -> { synchronized (objectLock) { objectLock.notify(); // 唤醒 System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } }, "B").start(); }
- B 线程先执行 objectLock.notify(),A 线程再执行 objectLock.wait(),这样 A 线程无法被唤醒
-
小总结
- wait和notify方法必须要在同步块或者方法里面且成对出现使用
- 先wait后notify才OK
2.2.2. Condition接口中的等待与唤醒
-
正常情况下
static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); private static void lockAwaitSignal() { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } finally { lock.unlock(); } }, "A").start(); new Thread(() -> { lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } finally { lock.unlock(); } }, "B").start(); }
- A 线程先执行,执行 condition.await() 后被阻塞,B 线程在 A 线程之后执行 condition.signal() 将 A线程唤醒
-
异常情况1:不在 lock() 和 unlock() 方法内使用 await() 和 signal() 方法
static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); private static void lockAwaitSignal() { new Thread(() -> { //lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } finally { //lock.unlock(); } }, "A").start(); new Thread(() -> { //lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } finally { //lock.unlock(); } }, "B").start(); }
- 不在 lock() 和 unlock() 方法内使用 await() 和 signal() 方法,将抛出 java.lang.IllegalMonitorStateException 异常
- 不在 lock() 和 unlock() 方法内使用 await() 和 signal() 方法,将抛出 java.lang.IllegalMonitorStateException 异常
-
异常情况2:先 signal() 后 await()
static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); private static void lockAwaitSignal() { new Thread(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); } finally { lock.unlock(); } }, "A").start(); new Thread(() -> { lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); } finally { lock.unlock(); } }, "B").start(); }
- B 线程先执行 condition.signal(),A 线程再执行 condition.await(),这样 A 线程无法被唤醒
2.2.3. 传统的 synchronized 和 Lock 实现等待唤醒通知的约束
-
线程先要获得并持有锁,必须在锁块(synchronized或lock)中
-
必须要先等待后唤醒,线程才能够被唤醒
2.3. LockSupport 类
- LockSupport 类使用了一种名为 permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit 只有两个值 1 和 0,默认是 0。
- 可以把许可看成是一种(0, 1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是 1。
2.3.1 阻塞
-
park()/park(Object blocker)
-
park() 方法的作用:阻塞当前线程/阻塞传入的具体线程
-
permit 默认是 0,所以一开始调用 park() 方法,当前线程就会阻塞,直到别的线程将当前线程的 permit 设置为 1 时,park() 方法会被唤醒,然后会将 permit 再次设置为 0 并返回。
-
park() 方法通过 Unsafe 类实现
// Disables the current thread for thread scheduling purposes unless the permit is available. public static void park() { UNSAFE.park(false, 0L); }
2.3.2 唤醒
-
unpark(Thread thread)
-
unpark() 方法的作用:唤醒处于阻断状态的指定线程
-
调用 unpark(thread) 方法后,就会将 thread 线程的许可 permit 设置成 1(注意多次调用 unpark()方法,不会累加,permit 值还是 1),这会自动唤醒 thread 线程,即之前阻塞中的LockSupport.park()方法会立即返回。
-
unpark() 方法通过 Unsafe 类实现
// Makes available the permit for the given thread public static void unpark(Thread thread) { if (thread != null) UNSAFE.unpark(thread); }
2.3.3 LockSupport 代码示例
-
正常使用情况
private static void lockSupportParkUnpark() { Thread a = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t" + "------come in"); LockSupport.park(); // 线程 A 阻塞 System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒"); }, "A"); a.start(); new Thread(() -> { LockSupport.unpark(a); // B 线程唤醒线程 A System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); }, "B").start(); }
- A 线程先执行 LockSupport.park() 方法将通行证(permit)设置为 0,其实这并没有什么鸟用,因为 permit 初始值本来就为 0,然后 B 线程执行 LockSupport.unpark(a) 方法将 permit 设置为 1,此时 A 线程可以通行
-
异常情况:没有考虑到 permit 上限值为 1
private static void lockSupportParkUnpark() { Thread a = new Thread(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + "------come in" + System.currentTimeMillis()); LockSupport.park(); LockSupport.park(); System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis()); }, "A"); a.start(); new Thread(() -> { LockSupport.unpark(a); LockSupport.unpark(a); System.out.println(Thread.currentThread().getName() + "\t" + "------通知"); }, "B").start(); }
- 由于 permit 的上限值为 1,所以执行两次 LockSupport.park() 操作将导致 A 线程阻塞
-
LockSupport 小总结
- LockSupport:俗称锁中断,LockSupport 解决了 synchronized 和 lock 的痛点
- LockSupport 不需要 Synchronized 或者 Lock ,不用持有锁块,不用加锁,程序性能好
- 无须注意唤醒和阻塞的先后顺序,不容易导致卡死
2.3.4 LockSupport 重点说明
-
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
- LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
-
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
-
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。
-
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。
-
每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证。
-
-
形象的理解
- 线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
- 当调用park方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
- 如果无凭证,就必须阻塞等待凭证可用;
- 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
2.4 LockSupport 面试题
2.4.1 为什么可以先唤醒线程后阻塞线程?
- 因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
2.4.2 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
- 因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
3. AQS
3.1 AQS 前置知识
公平锁和非公平锁
可重入锁
LockSupport
自旋锁
数据结构之链表
设计模式之模板设计模式
3.2 AQS 是什么?
-
字面意思
- AQS(AbstractQueuedSynchronizer):抽象的队列同步器
- 一般我们说的 AQS 指的是 java.util.concurrent.locks 包下的 AbstractQueuedSynchronizer,但其实还有另外三种抽象队列同步器:AbstractOwnableSynchronizer、AbstractQueuedLongSynchronizer 和 AbstractQueuedSynchronizer
-
技术翻译
- AQS 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量(state)表示持有锁的状态
- CLH:是一个双向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO (由 Craig、Landin and Hagersten 这三个大牛名字组成)
3.3 AQS 是 JUC 的基石
-
常见示例
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore
…… -
进一步理解锁和同步器的关系
- 锁,面向锁的使用者。定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可,可以理解为用户层面的 API。
- 同步器,面向锁的实现者。比如Java并发大神Douglee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等,Java 中有那么多的锁,就能简化锁的实现啦。
3.4 AQS 能干嘛
-
AQS:加锁会导致阻塞
有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
3.5 AQS 初步认识
3.5.1 AQS初识
-
为实现阻塞锁和相关的同步器提供一个框架,它是依赖于先进先出的一个等待
-
依靠单个原子int值来表示状态,通过占用和释放方法,通过改变状态值来获得锁
-
有阻塞就需要排队,实现排队必然需要队列
- AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
- Node 节点是啥?答:你有见过 HashMap 的 Node 节点吗?JDK 用 static class Node<K,V> implements Map.Entry<K,V> { 来封装我们传入的 KV 键值对。这里也是一样的道理,JDK 使用 Node 来封装(管理)Thread
- 可以将 Node 和 Thread 类比于候客区的椅子和等待用餐的顾客
3.5.2 AQS内部体系架构
-
AQS的int变量 —— state
AQS的同步状态State成员变量,类似于银行办理业务的受理窗口状态:零就是没人,自由状态可以办理;大于等于1,有人占用窗口,等着去 -
AQS的CLH队列
CLH队列,为一个双向队列,类似于银行侯客区的等待顾客 -
内部类Node(Node类在AQS类内部)
Node的等待状态waitState成员变量,类似于等候区其它顾客(其它线程)的等待状态,队列中每个排队的个体就是一个Node- Node类的内部结构
static final class Node{ //共享 static final Node SHARED = new Node(); //独占 static final Node EXCLUSIVE = null; //线程被取消了 static final int CANCELLED = 1; //后继线程需要唤醒 static final int SIGNAL = -1; //等待condition唤醒 static final int CONDITION = -2; //共享式同步状态获取将会无条件地传播下去 static final int PROPAGATE = -3; // 初始为0,其他几种状态就是上面的,CANCELLED 、SIGNAL 、CONDITION 、PROPAGATE volatile int waitStatus; // 前置节点 volatile Node prev; // 后继节点 volatile Node next; // ...
- Node类的内部结构
-
总结
- 有阻塞就需要排队,实现排队必然需要队列,通过state 变量 + CLH双端 Node 队列实现
3.5.3. AQS同步队列的基本结构
3.5.4 AQS底层是怎么排队的?
通过调用 LockSupport.pork() 来进行排队
3.6 从 ReentrantLock 开始解读 AQS
3.6.1 ReentrantLock 实现关系
- ReentrantLock 类是 Lock 接口的实现类,基本都是通过【聚合】了一个【队列同步器 Sync】的子类完成线程访问控制的
- Sync 类又继承了 AQS
- 公平锁、非公平锁也是基于 Sync
3.6.2 公平锁 & 非公平锁
- 在 ReentrantLock 内定义了静态内部类,分别为 NoFairSync(非公平锁)和 FairSync(公平锁)
- ReentrantLock 的构造函数:不传参数表示创建非公平锁
- 参数为 true 表示创建公平锁;参数为 false 表示创建非公平锁
公平与非公平锁,是如何获得锁的
-
lock() 方法的执行流程:以 NonfairSync 为例
-
公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件: hasQueuedPredecessors()
-
hasQueuedPredecessors() 方法是公平锁加锁时判断等待队列中是否存在有效节点的方法
公平锁与非公平锁的总结
-
对比公平锁和非公平锁的tryAcqure()方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
- 公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
- 非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一 个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)
-
而 acquire() 方法最终都会调用 tryAcquire() 方法
-
在 NonfairSync 和 FairSync 中均重写了其父类 AbstractQueuedSynchronizer 中的 tryAcquire() 方法
3.7 AQS 源码解读
- 整个 ReentrantLock 的加锁过程,大致分为三个阶段:
- 尝试加锁
- 加锁失败,线程入队列
- 线程入队列后,进入阻塞状态
先从示例代码入手
-
源码解读比较困难,我们这里举个栗子,假设 A、B、C 三个人都要去银行窗口办理业务,但是银行窗口只有一个个,我们使用 lock.lock() 模拟这种情况
public class AQSDemo { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); // 带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制 // 3个线程模拟3个来银行网点,受理窗口办理业务的顾客 // A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理 new Thread(() -> { lock.lock(); try { System.out.println("-----A thread come in"); try { TimeUnit.MINUTES.sleep(20); } catch (Exception e) { e.printStackTrace(); } } finally { lock.unlock(); } }, "A").start(); // 第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待, // 进入候客区 new Thread(() -> { lock.lock(); try { System.out.println("-----B thread come in"); } finally { lock.unlock(); } }, "B").start(); // 第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待, // 进入候客区 new Thread(() -> { lock.lock(); try { System.out.println("-----C thread come in"); } finally { lock.unlock(); } }, "C").start(); } }
3.7.1 从非公平锁的 lock() 入手
- 之前已经讲到过,new ReentrantLock() 不传参默认是非公平锁,调用 lock.lock() 方法最终都会执行 NonfairSync 重写后的 lock() 方法
先来看看线程 A(客户 A)的执行流程
-
第一次执行 lock() 方法
-
由于第一次执行 lock() 方法,state 变量的值等于 0,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1) CAS 判断,可得 state == expected == 0,因此 CAS 成功,将 state 的值修改为 1
-
setExclusiveOwnerThread() :将拥有 lock 锁的线程修改为线程 A
再来看看线程 B(客户 B)的执行流程
第二次执行 lock() 方法
- 由于第二次执行 lock() 方法,state 变量的值等于 1,表示 lock 锁没有被占用,此时执行 compareAndSetState(0, 1) CAS 判断,可得 state != expected,因此 CAS 失败,进入 acquire() 方法
- 之后进入队列同步器进行排队
3.7.2 acquire()
- 通过上面所述,当线程 B 发现锁已经被占用,那么走进了 acquire 方法
3.7.3 tryAcquire(arg) :尝试抢占 arg 个线程
-
在 tryAcquire 中没有其他内容,直接抛出了异常,这就是设计模式中的 模板设计模式,也即所有 AQS 的子类必须实现该方法,否则就抛异常
-
查看所有实现类
-
这里以 ReentrantLock 的非公平锁 NonfairSync 为例,在 tryAcquire() 方法中调用了 nonfairTryAcquire() 方法,注意,这里传入的参数都是 1
-
nonfairTryAcquire(acquires) 正常的执行流程:
-
在 tryAcquire() 方法返回 false 之后,取反后为 true,那么会继续执行 addWaiter() 方法
3.7.4 addWaiter(Node.EXCLUSIVE) :进入等候区
-
之前讲过,Node 节点用于封装用户线程,这里 Node.EXCLUSIVE 就是之前说的 Node 对象中的属性,排他
-
注意:哨兵节点和 nodeB 节点的 waitStatus 均为 0,表示在等待队列中
-
上图文字的示意图
-
第一次执行 for 循环:当线程 B 进来时,双端同步队列为空,此时肯定要先构建一个哨兵节点。并头指针指向哨兵节点,尾指针也指向该哨兵节点
-
第二次执行 for 循环:将装着线程 B 的节点 (NodeB) 放入双端同步队列中。以尾插法的方式,先将 NodeB 设为尾节点,再将 NodeB 的前指针指向 NodeNew,接着将 NodeNew 的后指针指向 NodeB
-
此时,线程C 进来了,线程 C 和线程 B 的执行流程很类似,都是执行 acquire() 中的方法
-
但是在 addWaiter() 方法中,执行流程有些区别。此时 tail != null,因此在 addWaiter() 方法中就已经将 nodeC 添加至队尾了,不需要再执行 enq(node) 方法
-
NodeC 入队示意图
3.7.5 acquireQueued(node, arg)
执行完 addWaiter() 方法之后,就该执行 acquireQueued() 方法了
-
线程 B 的执行流程
-
线程 B 执行 addWaiter() 方法之后,就进入了 acquireQueued() 方法中,此时传入的参数为封装了线程 B 的 NodeB
-
NodeB 的前驱结点为哨兵节点,因此 node.predecessor() 获取到的哨兵节点。
-
哨兵节点满足 p == head,但是线程 B 执行 tryAcquire(arg) 方法尝试抢占 lock 锁时还是会失败,因此会执行下面 if 判断中的 shouldParkAfterFailedAcquire(p, node) 方法
-
哨兵节点的 waitStatus == 0,因此执行 CAS 操作将哨兵节点的 waitStatus 改为 Node.SIGNAL(-1)
-
执行完毕将退出 if 判断,又会重新进入 for( ; ; ) 循环,此时执行 shouldParkAfterFailedAcquire(p, node) 方法时会返回 true,因此此时会接着执行 parkAndCheckInterrupt() 方法
-
线程 B 调用 park() 方法后被挂起,程序不会然续向下执行,程序就在这儿排队等待
-
-
线程 C 的执行流程,大致相同
3.7.6 unlock()
线程 A 执行 unlock() 方法
-
A 线程终于要 unlock() 了吗?真不容易啊!
-
unlock() 方法调用了 sync.release(1) 方法
-
release() 方法的执行流程
-
执行完上述操作后,当前占用 lock 锁的线程为 null,哨兵节点的 waitStatus 设置为 0,state 的值为 0(表示当前没有任何线程占用 lock 锁)
杀个回马枪:继续来看 B 线程被唤醒之后的执行逻辑
-
再次回到 lock() 方法的执行流程中来,线程 B 被 unpark() 之后将不再阻塞,继续执行下面的程序
-
执行完 setHead(node) 方法的状态如下图所示
-
将 p.next 设置为 null,这是原来的哨兵节点就是完全孤立的一个节点,此时 nodeB 作为新的哨兵节点
3.7、AQS 总结
3.7.1 AQS 的考点
-
第一个考点:我相信你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?
答:3个状态:没占用是0,占用了是1,大于1是可重入锁
-
第二个考点:如果锁正在被占用,AB两个线程进来了以后,请问这个总共有多少个Node节点?
答:答案是3个,分别是哨兵节点、nodeA、nodeB
3.7.2 AQS 源码解读案例图示
《尚硅谷Java大厂面试题第3季》- 周阳 - AQS 流程图:https://www.processon.com/view/link/64449ade84b4b71c14fe1079