目录
2.1.3 ReentrantReadWriteLock 读写锁
5.1 Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
5.4.4 addWaiter(Node.EXCLUSIVE)
5.4.5 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
前提了解:
公平锁和非公平锁
可重入锁
LockSupport
自旋锁
数据结构之链表
设计模式之模板设计模式
1 是什么
抽象的队列同步器
队列:单/双列表, ArrayList就是链表结构
是用来构建锁或者其他同步器组件(一系列根据锁相关的组件)的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。
JUC中有几个AQS缩写的相关内容,长得很相似,通常聊的AQS是指AbstractQueuedSynchronizer
2 AQS是JUC内容中最重要的基石
2.1 和AQS相关的
2.1.1 ReentrantLock
2.1.2 CountDownLatch 计数器
2.1.3 ReentrantReadWriteLock 读写锁
2.1.4 Semphore 信号灯
.。。。。。。还有很多
2.2 锁和同步器的关系
锁:面向锁的使用者:定义了程序员和锁交互的使用层API
同步器:比如java并发大神Douglee,面向锁的实现者:统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。
3 能做什么?
3.1 加锁会导致阻塞
有阻塞就需要排队,实现排队必须要有某种形式的队列来进行管理。
3.2 解释
4 AQS初步
4.1 AQS解释说明
有阻塞就需要排队,实现排队必然需要队列
AbstractQueuedSynchronizer里面,排队的线程会装到Node类里
4.2 AQS内部体系架构
4.2.1 AQS自身
4.2.1.1 AQS的int变量
AQS的同步状态state成员变量:
银行办理业务的手里窗口状态:
- 0就是没有人,自由状态可以办理
- 大于等于1,有人占用窗口,等着去排队
4.2.1.2 AQS的CLH队列
CLH队列(三个大牛名字组成),为一个双向队列
银行候客区的等待顾客
4.2.1.3 小总结
有阻塞就需要排队,实现排队必然需要队列
state变量+CLH变种的双端队列
4.2.2 内部类Node(Node在AQS类的内部)
4.2.2.1 Node的int变量
- node的等待状态waitState成员变量: volatile int waitStatus
- 解释:等候区其他顾客(其他线程)的等待状态;队列中每个排队的个体就是一个node.
4.2.2.2 Node类讲解-内部结构
4.2.2.3 Node类讲解-属性说明
4.3 AQS同步队列的基本结构
5 从ReentrantLock聊AQS
5.1 Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
lock unlock 底层都是sync.lock(), sync.release()
sync是ReetrantLock的一个内部类,sync又继承了AQS抽象队列同步器
5.2 ReentrantLock的原理
5.3 lock方法看公平和非公平
Lock lock = new ReentrantLock();
默认不传值是非公平锁
5.4 非公平锁,方法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 (InterruptedException e) { e.printStackTrace(); }
} finally {
lock.unlock();
}
}, "A").start();
//第2个顾客,第2个线程->由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待
//进入候客区
new Thread(() -> {
lock.lock();
try {
System.out.println("-----B thread come in");
} finally {
lock.unlock();
}
}, "B").start();
//第3个顾客,第3个线程->由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待
//进入候客区
new Thread(() -> {
lock.lock();
try {
System.out.println("-----C thread come in");
} finally {
lock.unlock();
}
}, "C").start();
}
}
5.4.1 lock()
5.4.2 acquire()
A线程执行后,B线程执行,由于A已抢占,B只能排队,执行acquire(1)
5.4.3 tryAcquire()
AQS设定了tryAcquire方法,在子接口进行实现,查看非公平lock
getState,由于A已经改成了1,越过if代码块,当前线程为B,占用线程为A,两者不相等,越过else if代码块,返回false。!tryAcquire(arg) 值取反 = true, 继续下一个方法 addWaiter()
若是后续线程执行该方法 满足if、elseif 中的条件 最后返回true的话,结束,不执行addWaiter
5.4.4 addWaiter(Node.EXCLUSIVE)
tail是尾节点,当前队列中没值,尾节点为null,故不执行if中的代码,执行enq(node)
节点tail为空,故创建了一个node节点(初始化一个占位哨兵节点),头节点指向node,尾节点指向了头节点
循环第二次时, tail已经有了哨兵(占位)节点的数据,走else代码
将哨兵节点作为当前线程node的前指针指向,将尾节点改为当前线程的node节点,也就是B线程
将哨兵节点的下一指针指向当前线程节点。
返回。
即:双向链表中,第一个节点为虚节点(也交哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。
接下来线程C重复B线程做的操作。
C节点执行addWaiter时,tail已经有值了,尾指针目前有指向B,故执行if中的代码。
C的前指针 = prev, prev目前=tail,故C的前指针指向了B。
尾指针指向调整为C。
返回。
源码和三大流程走向
5.4.5 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
B执行进入,获取前节点,哨兵节点。
p == head 判断前一节点是否等于头节点,当前B在执行,C还未执行,B的前节点是哨兵节点也就是头节点,故 = true . 再次尝试tryAcquire,看下前一线程是否释放,也就是A是否结束。
如果A没有结束,tryAcquire = false, 执行shouldParkAfterFailedAcquire, 在请求失败之后应该进行阻塞的方法。
此时waitStatus未改变过,初始化时是0,故执行最后的else方法,将前节点的waitStatus改为-1 (前节点目前是哨兵节点),即哨兵节点waitStatus = -1;
假设tryAcquire第二次抢占又失败了,又一次进入shouldParkAfterFailedAcquire方法,ws=Node.SIGNAL, 返回true.
执行sparkAndCheckInterrupt
B被阻塞,排队中,除非占用线程释放锁,B才会被唤醒执行。
C节点同B节点一样,执行到这里也会被阻塞。
5.5 方法unlock()
可以看到又是一个模板方法,需要由子类去实现
A线程要释放,获取state-1 = 0
free会从false改true
设置占用线程为null
设置state = 0;
返回true.
上层方法中if=true以后,
node节点 = 头节点
头节点是哨兵节点,非空,且waitStatus现在是-1, if语句为true,执行unparkSuccessor(头节点)
此时头节点waitStatus = -1, 将waitStatus改为0
获取哨兵节点的下一节点,目前是B节点
B节点不为null,B节点的waitStatus=0, 所以执行解锁。LockSupport.unpark(s.thread);
B被唤醒了。
回到之前B被lock的代码 parkAndCheckInterrupt返回true,B跳出if代码块,继续for循环
此时B的前节点p是头节点,再次tryAcquire
获取state = 0, 更改0为1, 设置当前占用节点为B线程
setHead(node);//将B作为head节点
B节点变成新的哨兵节点,原来的哨兵节点改为null,被GC回收掉
failed = false,后面finally代码块中,cancelAcquire(node);取消排队的代码不会被执行,因为中间没中断过。
结束。
6 总结(思维导图)【2】
7 参考文献
以上内容均来自下方视频,博客记录仅作为个人学习笔记使用
【1】Java面试_高频重点面试题 (第一、二、三季)_ 面试 第1、2、3季_柴林燕_周阳_哔哩哔哩_bilibili