【2020尚硅谷Java大厂面试题第三季 02】 AbstractQueuedSynchronizer抽象队列式同步器,synchronized,LockSupport,AQS源码解析
管程 和 锁
管程在功能上和信号量及PV操作类似,属于一种进程同步互斥工具,但是具有与信号量及PV操作不同的属性。
解析 一个管程是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块或软件包
乐观锁和悲观锁开
公平锁和非公平锁
可重入锁(又名递归锁)
死锁及排查
写锁(独占锁)读锁(共享锁)
自旋锁SpinLock
无锁→独占锁→读写锁→邮戳锁
无锁→偏向锁→轻量锁→重量锁
偏向锁 通俗的讲,偏向锁就是在运行过程中,对象的锁偏向某个线程。
(1) 为AQS打基础
AQS,全称是 AbstractQueuedSynchronizer,中文译为抽象队列式同步器。这个抽象类对于JUC并发包非常重要,JUC包中的ReentrantLock,,Semaphore,ReentrantReadWriteLock,CountDownLatch 等等几乎所有的类都是基于AQS实现的。
原文链接:https://blog.csdn.net/zxc123e/article/details/89684419
- Abstract Queued Synchronizer
AQS 中有两个重要的东西,一个以Node为节点实现的链表的队列(CHL队列),还有一个STATE标志,并且通过CAS来改变它的值。
AQS的这种设计使用的正是模板方法模式。
AQS支持线程抢占两种锁——独占锁和共享锁:
独占锁:同一个时刻只能被一个线程占有,如ReentrantLock,ReentrantWriteLock等,它又可分为:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
共享锁:同一时间点可以被多个线程同时占有,如ReentrantReadLock,Semaphore等
1、可重入锁
可重入锁(又名递归锁)
- 只要抢到了外层锁。内层锁 默认获得。
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象).不会因为之前已经获取过还没释放而阻塞。
Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
可:可以。
重:再次。
入:进入。
锁:同步锁。
进入什么
- 进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话
- 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次
进入。自己可以获取自己的内部锁
可重入锁种类
- 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
- Synchronized的重入的实现机理
- 显式锁(即Lock)也有ReentrantLock这样的可重入锁。
synchronized
- 同步块
- 同步方法
可重入锁:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
- 在一个synchronized修饰的方法或代码块的内部
调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
synchronized同步代码块
synchronized普通同步方法
synchronized静态同步方法
同步代码块 和 同步方法
static Object a = new Object();
public static void m1() {
new Thread(() -> {
synchronized (a) {
String name = Thread.currentThread().getName();
System.out.println(name + "外层调用");
synchronized (a) {
System.out.println(name + "中层调用");
}
}
}).start();
}
public static synchronized void m2() {
System.out.println("外层");
m3();
}
public static synchronized void m3() {
System.out.println("中层");
}
底层原理
javap -c xx.class 文件反编译
- 6: monitorenter
- 10: ldc
- 16: monitorexit 正常解锁
- 22: monitorexit 异常解锁
Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机
会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其
计数器加1,否则需要等待
,直至持有线程释放该锁。
- 锁对象的持有线程是当前线程,进入一层锁,计数器 +1
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
ReentrantLock
显式锁(即Lock)也有ReentrantLock这样的可重入锁。
由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。正常情况,加锁几次就要解锁几次
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println("外层");
lock.lock();
System.out.println("内层");
} finally {
lock.unlock();
}
}, "t1").start();
//如果上面不解锁,下面无法 加锁
new Thread(() -> {
lock.lock();//无法加锁
System.out.println("内层2");
}, "t2").start();
}
2、LockSupport
1为什么要学习LockSupport?
- 1.1 Java----JVM
- 1.2 JUC----AQS---->(前置知识可重入锁、LockSupport)
java.util.concurrent.locks
- LockSupport
定义
该类与使用它的每个线程关联一个许可证(在Semaphore类的意义上)。
https://www.apiref.com/java11-zh/java.base/java/util/concurrent/locks/LockSupport.html
如果许可证可用,将立即返回park ,并在此过程中消费; 否则可能会阻止。 如果尚未提供许可,则致电unpark获得许可。 (与Semaphores不同,许可证不会累积。最多只有一个。)可靠的使用需要使用volatile(或原子)变量来控制何时停放或取消停放。 对于易失性变量访问保持对这些方法的调用的顺序,但不一定是非易失性变量访问。
- 锁的支持
- 用于创建锁和其他同步类的基本线程阻塞原语。
- LockSupport中的park()和 unpark()的作用分别是阻塞线程和解除阻塞线程
- 线程等待唤醒机制(wait/notify的改良 加强版)
- synchronized
- wait/notify
- lock
- await/signal
wait和notify缺陷
Object类中的wait和notify方法实现线程等待和唤醒
new Thread(() -> {
synchronized (obj) {
String name = Thread.currentThread().getName();
System.out.println(name + "进入");
try {
obj.wait();
//1. 不加同步:Exception in thread "A" java.lang.IllegalMonitorStateException
//2. 先执行 notify 没意义。则后面的 wait 没人 唤醒,一直卡着。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "被唤醒成功");
}
}, "A").start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (obj) {
obj.notify();
System.out.println(Thread.currentThread().getName() + "唤醒");
}
}, "B").start();
2.2结论
0bject类中的wait、notify、notifyALL用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchron
ized) 。
先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒
- wait和notify方法必须要在同步块或者方法里面且成对出现使用
- 先wait后notify才oK
Condition 一样问题
Condition接口中的await后signal方法实现线程的等待和唤醒
static Object obj = new Object();
static Lock lock = new ReentrantLock();
static Condition cdt = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
String name = Thread.currentThread().getName();
System.out.println(name + "进入锁");
try {
cdt.await();
System.out.println(name + "被唤醒");
//1. 如果不加锁:Exception in thread "A" java.lang.IllegalMonitorStateException
//2. 先执行 signal 没意义。则后面的 await 没人 唤醒,一直卡着。
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
lock.lock();
try {
String name = Thread.currentThread().getName();
cdt.signal();
System.out.println(name + "唤醒线程");
} finally {
lock.unlock();
}
}, "B").start();
}
}
3种 让线程等待睡眠 和 总结
传统的synchronized和Lock实现等待唤醒通知的约束
- 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
3种让线程等待和唤醒的方法
- 方式1:使用object中的wait())
方法让线程等待,使用object
中的notify()方法唤醒线程 - 方式2:使用Juc包中Condition
的await()方法让线程等待,使
用signal()方法唤醒线程 - 方式3: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
LockSupport park 和 unpark
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。
可以把许可看成是一种(0.1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
LockSupport类中的park等待和unpark唤醒
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
- park()
除非许可证可用,否则禁用当前线程以进行线程调度。- park(Object blocker)
- 阻塞当前线程/阻塞传入的具体线程
- unpark(Thread thread)
如果给定线程尚不可用,则为其提供许可。- 唤醒处于阻塞状态的指定线程
permit默认是O,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park
方法会被唤醒,
然后会将permit再次设置为O并返回。
- 汽车到 门口会阻塞,
- 等待 其他人 发放一个许可证。
- 汽车离开,许可证 回收。
- 别的线程将当前线程的permit设置为1时,这个车进到门口,相当于直接就有了许可证,直接就会放行。
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还
是1)会自动唤醒thread线程,
即之前阻塞中的LockSupport.park()方法会立即返回。
代码
Thread a = new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + "进入");
LockSupport.park();
System.out.println(name + "被唤醒");
}, "a");
a.start();
Thread b = new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + "唤醒a线程");
LockSupport.unpark(a);
}, "b");
b.start();
之前错误的先唤醒后等待,LockSupport照样支持
- 只需要把 a.start(); 调到最后一行 即可。
- 此时:LockSupport.park();,这行代码 执行为0毫秒,相当于 被注释掉了。方法形同虚设无效,时间—样
源码
//调用LockSupport.park()时
public static void park() {
UNSAFE.park(false, 0L);
}
public native void park(boolean var1, long var2);
//调用LockSupport.unpark()时
public static void unpark(Thread thread) {
if (thread != null){
UNSAFE.unpark(thread);
}
}
public class LockSupport {
private LockSupport() {}
// Hotspot implementation via intrinsics API
private static final sun.misc.Unsafe UNSAFE;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
}
}
}
源码分析
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也
有对应的唤醒方法。归根
结底,LockSupport调用的Unsafe中的native代码。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,
调用一次park会消费permit,也就是将1变成o,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把p
ermit置为1。
每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。
- unpark 发放通行证
- park 消耗通行证
形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
当调用park方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出;
*如果无凭证,就必须阻塞等待凭证可用;
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
面试题
为什么可以先唤醒线程后阻塞线程?
因为unpark获得了1个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻
塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次 unpark和调用一次unpark效果一样,只会增加
个凭证;
而调用两次park却需要消费两个凭证,证不够
不能放行。
- 放行的时候间隔 1秒,就行了。 相当于 加锁park,放行unpark,在加锁park,在放行unpark
LockSupport.unpark(a);
Thread.sleep(1000);
LockSupport.unpark(a);
(2) AQS 开始
Abstract Queued Synchronizer,中文译为抽象 队列式 同步器
-
AQS =state变量+CLH变种的双端队列
-
抽象的队列同步器
-
抽象类
-
队列:单链表 和 双链表
前置知识:
公平锁和非公平锁可重入锁
LockSupport自旋锁
数据结构之链表
设计模式之模板设计模式
- 多个抽象类,父类定义的足够高,
- 落地的实现,放在子类,形成钩子程序。
寻找AQS类
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
}
}
package java.util.concurrent.locks.AbstractQueuedSynchronizer ;
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
}
// AbstractQueuedLongSynchronizer
public abstract class AbstractQueuedLongSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
}
// AbstractOwnableSynchronizer 父类
owned
英
/əʊnd/
v.
拥有;承认;(非正式)彻底击败(own 的过去式和过去分词)
adj.
自身拥有的
own
英
/əʊn/
v.
有,拥有;<旧> 承认;为(某事)承担全责
adj.
自己的,属于自己的;自己做的,为自己的
pronoun.
自己的,属于自己的;自己做的,自己生产的
基本说明
// doAcquireNanos 尝试的抢占锁
// release 释放
是用来构建锁或者其它同步器组件的重量级基础框架及整个Juc体系的基石,通过内置的FIFo队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态
- 信号灯:为0 没人持有锁。为1 说明有人在持有这个锁。
- 叫做:state 资源,决定锁 占用 和 未占用状态。
- 各线程 抢锁,抢不到排队。下一次 再次尝试 抢占锁。
CLH: Craig、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO
- 有 head 和 tail
tail
英
/teɪl/
n.
(动物的)尾巴;(一些蝴蝶的)狭翅须;(物体的)尾状物,尾部;(离去事物的)末尾部分;<非正式>盯梢人,跟踪者;
v.
跟踪,盯梢;(飞行物体)曲线移动; 拉紧(绞索);除去(水果或蔬菜的)梗
AQS为什么是JUC内容中最
重要的基石
CountDownLatch();//秦灭六国,一统华夏
CyclicBarrier();//集齐7龙珠,召唤神龙
Semaphore(); //5个车 抢3个车位
ReentrantReadWriteLock();
ReentrantLock()
从我们的ReentrantLock开始解读AQS
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
}
}
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
abstract static class Sync extends AbstractQueuedSynchronizer {}
}
public class Semaphore implements java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {}
}
锁 和 同步器
锁,面向锁的使用者
- 定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。
同步器,面向锁的实现者
-
统一的 管理。
-
比如Java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。
-
并发包的编写者大神。大神最搞笑的是,把 List 当做 Set,无序的东西,做成有序。
- CopyOnWriteArraySet 使用自己写的List
public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }
加锁会导致阻塞
- 有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(
类似银行业务办理窗口都满了
,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区
.的顾客也在等着叫号,轮到
了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,
将暂时获取不到锁的线样州八
到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋
以及LockSupport.park()的方式
,维护state变量的状态,使并发达到同步的控制效果。
AQS 初识
为实现阻塞锁和相关的同步器提供—个框架,它是依赖于先进先出的一个等待
依靠单个原子int值来表示状态,通过占用和释放方法,改变状态值
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//丢入的是 一个一个的线程。最终 封装成 Node 节点。Node<Thread>
static final class Node {
}
private transient volatile Node head;
private transient volatile Node tail;
//The synchronization state.
private volatile int state;
}
HashMap的 put方法,放入的是 Node<K,V>[] tab;
//数组 + 链表 + 红黑树
//k,v 键值对 类型的 Node数组。Node是一个静态内部类,模拟的链表。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
有阻塞就需要排队,实现排队
必然需要队列
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
体系架构
Lock
- ReentrantLock
- NonfairSync 都继承了 Sync
- FairSync
Serializable
- AbstractQueuedSynchronizer
- Sync
- Node
- ConditionObject
static final class NonfairSync extends Sync {}
private final Sync sync; //依然是内部类
abstract static class Sync extends AbstractQueuedSynchronizer {}
AQS的int变量
- AQS的同步状态State成员变量
- 银行办理业务的受理窗口状态
- 零就是没人,自由状态可以办理
- 大于等于1,有人占用窗口,等着去
AQS的CLH队列
- CLH队列(三个大牛的名字组成),为一个双向队列
- CLH: Craig、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO
- Doug Lea 借鉴后,改为了 虚拟双向队列
- 通过自旋等待
- 会 看这个窗口,是否是0,是0 就是 释放了。
- state变量判断是否阻塞
- 从尾部入队
- 从头部出队
- 有阻塞就需要排队,实现排队必然需要队列
- AQS =state变量+CLH变种的双端队列
- 队列中的 元素Node = waitStatus +前后指针指向
- AQS =state变量+CLH变种的双端队列
- 银行候客区的等待顾客
Node
static final class Node {
static final Node SHARED = new Node();//共享。表示线程以共享的模式等待锁
static final Node EXCLUSIVE = null;//排他,独占。表示线程正在以独占的方式等待锁
static final int CANCELLED = 1;//线程被取消了。1,表示线程获取锁的请求已经取消了
static final int SIGNAL = -1;//后继线程需要唤醒。-1表示线程已经准备好了,就等资源释放了
//表示线程正在等待状态
static final int CONDITION = -2;//等待 condition 唤醒。表示节点在等待队列中,节点线程等待唤醒
static final int PROPAGATE = -3;//共享式 同步状态获取 将会无条件地 传播下去。
//当前线程处在SHARED情况下,该字段才会使用
//Node的等待状态waitState成员变量
//等待状态。每个等待队列里,线程的状态
//初始为0,状态为 上面的 几种。0—个Node被初始化的时候的默认值
//当前节点在队列中的状态
volatile int waitStatus;
//前指针。前置节点。
volatile Node prev;
//后指针。后继节点。
volatile Node next;
//就是这个线程
//表示处于该节点的线程
volatile Thread thread;
//指向下一个处于CONDITION状态的节点。condition
Node nextWaiter;
//返回前驱节点,没有的话抛出npe
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
//头指针
private transient volatile Node head;
//尾指针
private transient volatile Node tail;
//这个是 AQS的 state。0没人占用,1有人占用着呢
private volatile int state;
等候区其它顾客(其它线程)的等待状态
- 队列中每个排队的个体就是一个Node
exclusive
英
/ɪkˈskluːsɪv/
adj.
独有的,专用的;高档的,昂贵的;不含……的;排外的;排斥的;全部的;唯一关心的;
n.
独家新闻,独家报道
state状态位
同步器
- head
- fail
Node1 里面封装就是 一个一个的线程。
- Prve
- next
Node2
corPareAndSefTail()
(3) AQS源码
从我们的ReentrantLock开始解读AQS
Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
公平锁 和 非公平锁区别
- 公平锁 多了 , 判断有没有排队 方法,如果有排队,则排到最后面。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
}
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//这句重要,公平锁 判断有没有排队。如果返回true,取反为false,说明有在排队。不抢占。
//如果返回 false,说明没有在排队。取反为true,直接抢占。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
return false;
}
}
//如果返回true,说明有在排队。
//如果返回false,说明没有在排队。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//尾结点 != 头结点。如果:没有排队 头结点和尾结点相同。
//&& h.next == null。有排队下个指针,一定不为null。
//null != null 返回的 false,
//s.thread != Thread.currentThread() 如果是当前线程 就是下一个线程,肯定是相同的,代码说不相同,直接返回false
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
}
//非公平锁是是直接抢占
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
可以明显看出公平锁与非公平锁的lock()方法唯[的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors()
hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法
lnterface Lock
-
ReentrantLock
- Sync 继承:AbstractQueuedSynchronizer
- FairSync
- NonfairSync
- Sync 继承:AbstractQueuedSynchronizer
-
//默认创建非公平锁
-
//创建的是非公平锁 传递false
-
//创建的是公平锁 传递true
非公平锁 查看源码
对比公平锁和非公平锁的 tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!
hasQueuedPredecessors()
predecessor
英
/ˈpriːdəsesə(r)/
n.
前任,前辈;(被取代的)原有事物,前身
hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下;
公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会
进入等待队列中;
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark()
之后还是需要竞争锁(存在线程竞争的情况下)
在创建完公平锁/非公平锁后,调用lock 方法会进行加锁,
最终都会调用到acquire方法
- 非公平锁 抢占不到,设置为了1
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//设置为了 1。最核心的代码。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//供他人重写的
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
}
整个 ReentrantLock 的加锁过程,可以分为三个阶段:
1、尝试加锁;
2、加锁失败,线程入队列;
3、线程入队列后,进入阻塞状态。
- 等着被 叫号,在从阻塞队列恢复。
对应下面1②③三部分。
重头再来 非公平锁
3个人来办理业务
//带入一个银行办理业务的案例来模拟我们的AQs如何进行线程的管理和通知唤醒机制
ReentrantLock lock = new ReentrantLock(false);
//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
new Thread(() -> {
lock.lock();
try {
System.out.println("A顾客过来办理");
try {
TimeUnit.MINUTES.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}, "A").start();
//第2个顾客,第2个线程---->,由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
//进入候客区
new Thread(() -> {
lock.lock();
try {
System.out.println("B顾客过来办理");
} finally {
lock.unlock();
}
}, "B").start();
// C一样,A办理完毕,C和B会去抢
new Thread(() -> {
lock.lock();
try {
System.out.println("C顾客过来办理");
} finally {
lock.unlock();
}
}, "C").start();
三个顾客来银行办理业务,
类似三个线程ThreadA ThreadB ThreadC
AQS = state + CLH队列
窗口每次只能
服务一个顾客,初始没人
- state = 0
窗口每次只能服务一个顾客,初始没人
- 银行受理业务窗口Thread = null
下面虚线是模拟银行的等候区,每个线程就是一个顾客
1. 进入ReentrantLock的方法
public void lock() {
sync.lock();
}
2. 进入Sync类 即:AQS的实现类
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
}
3. 寻找到Sync的 实现类,非公平锁的lock()
- 找到 lock的实现
static final class NonfairSync extends Sync {
final void lock() {
//我期望是0,设置为1。 0代表,没有人在办理
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
4. 比较并设置 state 是 抽象队列同步器
-
即:AQS的 最最底层,依然是 unsafe ,CAS 思想。
-
AbstractQueuedSynchronizer
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
private volatile int state;//这里设置的 this,就是 设置的 当前对象的 state 属性,
//可能是 C++,默认代码 会自动选择 当前对象的state 吧
5. 设置0成功,调用父类的方法 设置占用
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
//设置,占用这个 窗口的线程
private transient Thread exclusiveOwnerThread;
//参数为:Thread.currentThread()
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
}
6. 线程A 会一直占用,分析线程B
7. 线程B 走NonfairSync 另一个分支
- 发现有人
final void lock() {
//我期望是0,设置为1。 0代表,没有人在办理。现在有人。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//一次 compareAndSetState 失败,走到这里:
acquire(1);//参数为1,一个窗口 只能服务一个人。
}
8. 调用 抽象队列同步器的 acquire。3大流程
-
A一直占用,B是首个过来。会抢占几次。3次。第一次是tryAcquire。acquireQueued里抢2次。
-
AbstractQueuedSynchronizer
public final void acquire(int arg) {//参数为1
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire
英
/əˈkwaɪə(r)
v.
获得,得到;学到,习得;患上(疾病);逐渐具有,开始学会
9. 抽象队列同步器tryAcquire,最终调用重写方法
//AbstractQueuedSynchronizer
protected boolean tryAcquire(int arg) {
//模板设计方法设计模式,钩子方法。所有子类必须实现这个方法,不实现 父类调用时,直接报异常。
//规范统一的 往这个模板去放。落地的实现,放到子类。
throw new UnsupportedOperationException();
}
- 发现被子类重写,走的 子类重新的方法 NonfairSync
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {//参数为1
return nonfairTryAcquire(acquires);
}
}
10. 核心方法1 非公平锁 TryAcquire
final boolean nonfairTryAcquire(int acquires) {//参数为1
//获得线程B
final Thread current = Thread.currentThread();
//获得 state的值。现在被线程1占用,肯定为1
int c = getState();
if (c == 0) {
//极端运气好:B准备进队列等待的时候,刚刚A办理完了。
if (compareAndSetState(0, acquires)) {//就设置当前占用窗口 为B
setExclusiveOwnerThread(current);//那 调用者 取反,就是为false
return true;
}
}
//判断当前线程 和 正在办理的线程A,是不是同一个。
else if (current == getExclusiveOwnerThread()) {
//极端情况:A离开了后,又是A抢到。相当于A刚走,又说 等等,我在办理一个业务。
int nextc = c + acquires;//原来的1,在+1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置为2,这里就是 可重入锁
return true;
}
//结果返回false
return false;
}
//上面 设置线程A的时候,用的是 set
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
11. 核心方法2 抽象队列同步器的 addWaiter
- tryAcquire方法结果为 false,取反 true,执行第二个方法
public final void acquire(int arg) {//参数为1
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 此时 B线程,正式的入队。
- 注意:等候队列的第一个节点,为哨兵节点 傀儡节点。 不是B节点。
- 双向链表中,第一个节点为虚节点(也叫哨兵节点),其
实并不存储任何信息,只是占位。 - 真正的第一个有数据的节点,是从第二个节点开始的。
// static final Node EXCLUSIVE = null;
// acquireQueued(addWaiter(Node.EXCLUSIVE), arg)//丢进去的是 排它的
private Node addWaiter(Node mode) {
//当前线程,为 线程B
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;//尾结点 取出来,现在都是为空,肯定为null
if (pred != null) {//null != null 为假
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//线程B,排他的,进入enq方法
enq(node);
return node;
}
//线程B,排他的,进入enq方法
private Node enq(final Node node) {
for (;;) {//自旋 进入。
Node t = tail;//尾指针,赋值给t
if (t == null) { // Must initialize
//尾指针第一次,现在 肯定为null,初始化。
//比较并设置头,new 了一个空节点。第一个节点,并不是 我们的 Node线程,而是空节点 作为占位符。
//如果尾指针没有,说明没有任何元素,进入到这个队列。新建一个node作为头结点
//new Node() 内:Thread = null,watiStatus = 0。傀儡节点,哨兵节点。占位。
if (compareAndSetHead(new Node()))
tail = head;//头节点,赋值给 尾结点。 此时:头尾节点 都指向了:new Node()
//再次进入循环:
// Node t = 尾结点,就是哨兵节点。
//不为null,进入下面的分支。
} else {
//B线程,真真正正的入队
//B节点的 前一个指针,指向t,B前面为t (现在为哨兵),没毛病。
node.prev = t;
//设置尾结点。希望尾结点为t(哨兵节点),更新为node(线程B)
//之后,尾指针 就会变成 第二个节点。
//尾结点的node为
//Thread = ThreadB 顾客B
//WaitStatus = 0
if (compareAndSetTail(t, node)) {
//哨兵节点的 下一个,为 线程B,没毛病。
t.next = node;
return t;
//此时:
//head 指向哨兵
//tail 指向 线程B
//哨兵节点的 next 指向 线程B
//线程B的 prev 指向 哨兵。
}
}
}
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
private final boolean compareAndSetHead(Node update) {//期望为null,才new
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
- B入队完毕
12. C开始执行,最终依然是 addWaiter
private Node addWaiter(Node mode) {
//线程C,依然是 独占的
Node node = new Node(Thread.currentThread(), mode);
//取出 尾指针
Node pred = tail;
//现在 肯定不为null
if (pred != null) {
//用这里代码进行入队。
//C的前一个指针,指向 原来尾指针(pred是原来的尾指针)。没毛病。
node.prev = pred;
//比较并设置尾指针,期望为:原来的尾指针,更新为:C线程。
if (compareAndSetTail(pred, node)) {
//原来的尾指针 的下一个 为C线程,没毛病。
pred.next = node;
//返回 node (线程C)
return node;
//线程B 的 next 指向了 C
//线程C 的 prev 指向了 B
//tail 尾指针,指向了 C
}
}
//C进来,没有进入 enq 方法
enq(node);
return node;
}
acquire 梳理
AQs acquire主要有三条流程
1.调用tryAcquire 首次抢占线程
- 交由子类FairSync实现
- 如果抢到了 返回true,取反 就结束 if
- 如果没有抢到,返回false,取反为true。执行 if的 下一个方法 addWaiter
2调用addWaiter 入队。
- 首次入队:enq入队操作
- 非首次入队, if (pred != null) { 分支
3.调用acquireQueued 主要是lock,候客区坐稳休息。会先抢占2次。
12. 核心方法3。B入队完毕,调用acquireQueued
public final void acquire(int arg) {//参数为1
if (!tryAcquire(arg) &&
//现在开始调用 acquireQueued,参数依然为 B线程。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
//线程B,排他的,进入enq方法
enq(node);//方法返回的为 前一个节点。并没有接收
return node;//返回 B线程。
}
final boolean acquireQueued(final Node node, int arg) {
//failed=true,如果最终 依然为true,取消排队
boolean failed = true;
try {
//业务被打断为 假。比如银行说,故障了,队伍排的都没用了。
boolean interrupted = false;
for (;;) {
//返回队列里面的第一个节点。(当前节点B的前一个为 哨兵节点)
final Node p = node.predecessor();
//p == head 为 true。再次尝试抢 tryAcquire,依然是 非公平锁实现的
if (p == head && tryAcquire(arg)) {//tryAcquire(arg)会返回false。因为A在办理着呢。这里不进入
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//再次抢占失败之后,应该阻塞
//p是 头结点,node 是当前节点 B
//第一次:shouldParkAfterFailedAcquire 返回了 false,不继续执行。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果最终 依然为true,取消排队
if (failed)
cancelAcquire(node);
}
}
//第二次 自旋:
for (;;) {
//node 依然为 B,B的前一个,依然为哨兵
final Node p = node.predecessor();
//再次抢占
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//再次执行,这个方法。哨兵节点,上面已经改为了-1,再次进入这个方法,会返回true,调用parkAndCheckInterrupt
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
//返回 前一个节点。当前为B过来的,B的前一个为 哨兵节点。
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//和 这里的是同一个方法
public final void acquire(int arg) {
if (!tryAcquire(arg)
}
//前置节点,现在为头结点
//node 为 当前节点。
//方法执行后,把哨兵节点的值,改为了 -1
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//哨兵节点的 waitStatus 为 0
int ws = pred.waitStatus;
// SIGNAL = -1; 0不等于 -1
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// waitStatus 为0 ,只能走到这里。
//设置 pred(头结点) 的值,希望为0,改为 -1。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
- 如果前驱节点的waitStatus是SIGNAL状态,即 shouldParkAfterFailedAcquire方法会
返回true
程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起
13. B自旋两次后进入 parkAndCheckInterrupt
-
两次抢后依然失败,加上最最开始抢的一次,一共抢3次失败。
-
这里才真正入队后,坐在 候客区的椅子上,休息了。
for (;;) {
//xx
//shouldParkAfterFailedAcquire返回true
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
private final boolean parkAndCheckInterrupt() {
//节点B,进入了 睡眠。最开始就是 开的B线程,里的锁对象 调用的所有方法。
LockSupport.park(this);//B线程,在这里 阻塞,正在排队等待中。被唤醒后,将要执行下面的代码。
//后面A,走了后,会唤醒的。
return Thread.interrupted();
}
//根据 park方法API描述,程序在下述三种情讽会继续向下执行
//1.被unpark
//2.被中断(interrupt)
//3.其他不合逻辑的返回才会继续向下执行
//因上述三种情况程序执行至此,返回当前线程的中断状态 并清空中断状态
//如果由于被中断,该方法会返回true
Thread.interrupted() 该方法用于检测线程是否中断,以及清除中断标志位。当第二次调用这个方法的时候就会返回false。
14. 最终C也会LockSupport.park
lock()
-
acquire(1);
-
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }- tryAcquire
- addWaiter
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
- addWaiter
- tryAcquire
A线程 unlock
1. ReentrantLock 的 unlock
public void unlock() {
sync.release(1);
}
2. 抽象队列同步器 释放
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3. 抽象队列同步器 的 tryRelease
- 依然是钩子方法,调用 非公平锁 类
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
public class ReentrantLock implements Lock, java.io.Serializable {
protected final boolean tryRelease(int releases) {
//传递的为1, 1-1 = 0
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//空闲为 false
boolean free = false;
if (c == 0) {//c=0,进入
free = true;//空闲设置为 真
//设置 当前窗口的占用线程,为null
setExclusiveOwnerThread(null);
}
setState(c);
return free;//返回true
}
}
//AbstractOwnableSynchronizer 最顶层父类的
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
//state 设置为0
protected final void setState(int newState) {
state = newState;
}
4. 继续执行 unparkSuccessor
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;//哨兵节点,赋值给h
if (h != null && h.waitStatus != 0)//哨兵节点的 等待状态为 -1
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//哨兵节点的 waitStatus 赋值给 ws,为 -1
int ws = node.waitStatus;
if (ws < 0)//-1当然小于0,又把 哨兵节点的 waitStatus 设置为0
compareAndSetWaitStatus(node, ws, 0);
//unsafe.compareAndSwapInt 找的应该是node对象里,根据地址找:值为 ws 的吧,设置为0
//哨兵节点的 下一个节点。就是 除了哨兵外的 第一个节点。B节点。
Node s = node.next;
if (s == null || s.waitStatus > 0) {//B节点的 waitStatus = 0,不进入
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null) //不能null,解锁:B节点。 上面 阻塞的,会唤醒。
LockSupport.unpark(s.thread);
}
B被唤醒后,继续执行
- B 没有被中断过,中断标志位 为false
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
1. 进入tryAcquire再次抢占
public final void acquire(int arg) {//参数为1
if (!tryAcquire(arg) &&
//现在开始调用 acquireQueued,参数依然为 B线程。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//B 的 前一个节点,依然是 哨兵节点
final Node p = node.predecessor();
//哨兵节点 肯定为 头节点,进入 抢占,会抢占成功的
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//没有被终端过,这里返回false。再次进入自旋
interrupted = true;
}
} finally {
//如果最终 依然为true,取消排队
if (failed)
cancelAcquire(node);
}
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
2. 非公平锁的tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//当前为 B
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//c现在,为0,因为a已经走了。
if (compareAndSetState(0, acquires)) {//希望是0,设置成1,设置成功
//现在被B,占用了 设置占用的线程为 B
setExclusiveOwnerThread(current);
//返回true
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
3. 继续执行acquireQueued下面的代码
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//B 的 前一个节点,依然是 哨兵节点
final Node p = node.predecessor();
//哨兵节点 肯定为 头节点,进入 抢占,会抢占成功的
if (p == head && tryAcquire(arg)) {//这里会返回true
//设置头节点,现在为B线程。同时B线程 已经抢到资源 了。
setHead(node);
//果然大神考虑的很全面,原来的 哨兵的下一个指向清空。
p.next = null; // help GC
failed = false;//失败为 false,表示 正常的办理业务
return interrupted;//返回 false
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//没有被终端过,这里返回false。再次进入自旋
interrupted = true;
}
} finally {
//失败为 false,不走取消排队的逻辑
if (failed)
cancelAcquire(node);
}
}
//传递的参数为 B线程
private void setHead(Node node) {
//头节点指向B
head = node;
//B的线程清空
node.thread = null;
//B的前一个节点清空。 相当于把 上一个哨兵节点删除了,B变成了哨兵节点。
node.prev = null;
}
4. 最终
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//最终 acquireQueued 返回false,不执行 selfInterrupt
总结
ReentrantLock
-
lock 底层走的是 : Sync 继承了 :抽象队列同步器
-
Sync的lock 是抽象的方法,分为 公平锁 和 非公平锁
abstract void lock(); -
比较并设置:如果窗口没人,state为0,那就最简单了,直接占用
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } -
false走到 acquire(1);
-
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } -
尝试获取锁,如果获取不到,排队(阻塞当前线程)
-
tryAcquire 返回true:表示 获取到了锁,线程逐级返回,加锁过程结束。
-
tryAcquire 返回true:返回false,表示线程没有获取到锁。
-
整体返回true,进入下一个方法 addWaiter
-
第一次初始化,先执行 enq(node),下面说的 2情况。
-
将当前线程封装成Node对象,并加入排队队列中。 根据排队队列是否执行过初始化。执行1、2不同处理逻辑。 1:表示排队队列不为空,即之前已经初始化过了,此时只需将新的node 加入排队队列末尾即可。 2:表示排队队列为空,需执行队列初始化。enq会初始化一个空的Node,作为排队队列的head,然后将需要排队的线程,作为head的next节点插入。-
enq方法解释:没有其他线程在排队,调用enq构造队列,并将node 加入到队列。返回node
- enq 主要是 初始化哨兵节点
-
AQS有两个属性head和 tail,分别用来保存aqs 队列的头尾结点。初始时,这两个属性都是null。当有一个线程排队
-
放在哨兵节点的后边
-
这个队列表示,在节点n2插入队列之前,没有其他节点在排队。注意,当前持有锁的线程,永远不会处于排队队列中。这是因为:当一个线程调用lock获取锁时,只有两种情况: 1)锁处于自由状态(state==0),且它不需要排队,通过cas立刻获取到了锁,这种情况,显然它不会处于排队队列中; 2)锁处于非自由状态,线程加入到了排队队列的队头(不包括head)等待锁。将来某一时刻,锁被其他线程释放,并唤醍这个排队的线程,线程唤醒后,执行tryAcquire,获取到了锁,然后重新维护排队队列,将自己从队列移出(acquireQueued方法)。 所以,不管哪种情况,持有锁的线程,永远不会处于排队列表中。
-
-
-
第二次 C节点进入时(B节点进入为首次):
-
addWaiter方法中: if (pred != null) { //队列已经初始化,将node 加入到队列末尾。哨兵——B——C node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } -
将当前线程封装成Node对象,并加入排队队列中。 根据排队队列是否执行过初始化,执行1、2不同处理逻辑。 1:表示排队队列不为空,即之前已经初始化过了,此时只需将新的node 加入排队队列末尾即可。 2:表示排队队列为空,需执行队列初始化。enq会初始化一个空的Node,作为排队队列的head,然后将需要排队的线程,作为head 的next节点插入。
-
-
-
addWaiter 方法完成。
-
acquireQueued 尝试在队列里,在抢 (抢2遍)
-
acquireQueued返回,表示线程获取到锁,线程逐级返回,加锁过程结束。
-
最终 要执行 park 逻辑
-
抢占失败走
-
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())//最终要睡眠的 interrupted = true; -
整个aqs的核心和难点之一。注意这里使用了for() 首先判断node 的前辈节点,是不是head,如果是,说明它是下一个可以获得锁的线程,则调用―次tryAcquire,尝试获取锁,若获取到,则将链表关系重新维护下(node设置为head,之前的head从链表移出),然后返回。 如果node的前辈节点不是head,或获取锁失败,再判断其前辈节点的waitState,是不是SIGNAL,如果是,则当前线程调用park,进入阻塞状态。如不是: 1、== 0,则设置为SIGNAL; 2、>0(==1),则表示前辈节点已经被取消了,将取消的节点,从队列移出,重新维护下排队链表关系。然后再次进入for循环,上面的逻辑重新执行一遍。 注意和doAcquirelnterruptibly方法对比,二者区别主要在,发现线程被中断过之后的处理逻辑。 -
//这个方法,接收两个Node对象参数:参数2是准备执行park操作的节点node,参数1是其前辈节点pred。 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //第二次,直接返回 true,也很重要。 return true; if (ws > 0) { } else { //第一次把 0 换成 -1,这个很重要。 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } 注意,这里是设置pred节点,而不是node节点的waitState。-1表示节点处于阻塞状态了。 为何每个线程进入该方法后,修改的是上一个节点的waitState,而不是自己修改自己的? 因为线程调用park后,无法设置自己的这个状态,若在调用park前设置,存在不一致的问题。所以,每个node的waitState,在后继加点加入时设置。 -
阻塞后
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } 两种情况,会导致阻塞结束: 1、持有锁的线程,释放锁后,将这个线程unpark 了。此时该线程,一定排在队列的队头(不包括 head节点); 2、线程被interrupt 了。(注意,在外部interrupt这个线程,不是抛出lnterruptException,这一点和sleep.wait阻塞不—样)
-
-
-
-
更多推荐



所有评论(0)