1.AQS简介
AQS全称为AbstractQueuedSynchronizer(抽象队列同步器)。AQS是一个用来构建锁和其他同步组件的基础框架,
使用AQS可以简单且高效地构造出应用广泛的同步器,例如ReentrantLock、Semaphore、ReentrantReadWriteLock和FutureTask等等。
2.原理
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,
即将暂时获取不到锁的线程加入到队列中。
3.CLH队列
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。
AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
4.博客配套视屏教程
网易云课堂在线学习:
https://study.163.com/course/courseMain.htm?share=2&shareId=400000000332026&courseId=1213602801
视屏课程目录:
a.值为0表示资源空闲可用,int型默认为0;
b.值大于0表示资源忙,不可用,有线程持有这把锁;
c.如果发生锁重入则值+1,可以结合代码分析;
d.注意不要把节点的等待状态混淆在一起了,代码示例:volatile int waitStatus;
state状态变动的情况:
1.获取锁时,使用cas将0改为1,代码示例:compareAndSetState(0, 1)
2.锁从入时,将累加锁的状态值,代码示例:setState(nextc);
3.设置节点为下一个获取的节点,compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
AQS中的队列是先进先出的双向链表;
队列中的头节点并不是要获取锁的节点,只是占位而已,真正要获取锁的节点是第二个节点,第二个节点获取到锁之后成为头节点;
某个线程没有获取到锁则需要进入队列中等候,持有锁的线程一定不会在队列中,可以结合后面的代码分析
队列中的每个Node节点由主要由4个成员变量组成:
1.前一个节点指针
2.后一个节点指针
3.当前线程
4.当前线程的等待状态(WaitStatus)
对于 waitStatus 枚举值,记录当前线程的等待状态,
int型默认值为0
CANCELLED (1)表示线程被取消了
SIGNAL (-1)表示线程需要被唤醒,处于等待状态,即下一个获取资源的线程
CONDITION (-2)表示线程在条件队列里面等待
PROPAGATE (-3)表示释放共享资源时需要通知其他节点
注意:在后面的代码分析中一定要注意waitStatus的状态时如何修改的
简要代码如下:
在分析源码之前,我们再次回顾一下原理:
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,
即将暂时获取不到锁的线程加入到队列中。
用通俗的话说就是:
获取锁时如果资源空闲,就立即执行,执行完成后唤醒队列中等待的线程;如果资源被占用,就进入队列等待;(知道了这个大体方向后,对代码的理解非常有帮助)
源码分析开始:
1.创建锁对象调用lock方法
2.进入lock方法
3.公平锁和非公平锁选择
大家在这里可留意一下,公平锁和非公平锁是怎么样实现的的?
简单补充一下:
公平锁就是先到先获得资源的意思,在获取锁时会判断一下队列中有没有处于等待的线程(具体的代码实现后面看,这里简单提一下);
非公平锁是,只要能抢到锁都可以;
由此可见,非公平锁效率要高一点,实际生产中也是经常采用非公平锁,
那么问题又来了,我们在使用的时候怎么设置使用公平锁还是非公平锁呢?(详见后面代码解读)
4.调用acquire方法
传入参数1,表示将AQS中的资源状态state修改为1,加锁成功.
5.调用tryAcquire方法
6.进入公平锁重写的方法
注意:如果FairSync对象没有重写tryAcquire方法就会抛出异常
代码:
图解:
第一步:Node pred=tail; 将尾节点赋值给前置节点,意思结束pred指向t4;
第二步:node.pred=pred;设置新增节点t5的前置节点为pred,意思就是说设置t5的前置节点为t4;
注意:这里新增一个节点的时候,是先设置前置节点,如果在并发的情况下,
cpu发生切换,双向链表只能从后往前遍历,这个在面试中经常问到,如下图:
代码:
图解:
这个方法比较难理解
1.需要结合for自旋逻辑阅读代码
2.需要理解这三个方法的含义
shouldParkAfterFailedAcquire(p, node)检查当前节点是否应该被,阻塞等待park
parkAndCheckInterrupt() 当前线程进入阻塞状态
cancelAcquire(node); 取消节点
代码:
代码解读:
图解:
这个方法每次执行节点状态是动态改变的,单用静态图不好画,这里只是画出最核心的逻辑,
假设新增节点时node,前置节点时n3,并且n3是一个取消节点
图解:
这里假设当前取消的节点时N4,且N3是已经取消的节点,其他节点都是正常节点
代码解读:
代码解读:
代码解读:
代码解读:
测试代码:
测试预期结果:
设置断点:
线程状态图:
t0释放锁唤醒t1:
t1释放锁唤醒t2
...后面的流程就以此类推,直到所有的节点都执行完成
AQS的核心思想是当没有获取到资源的线程就在队列中等待,已经获取到资源的线程执行完成后就唤醒队列中的下一个节点去获取资源,以此循环;
根据这个设计思想,就涉及到:
如何让一个Java线程暂停,
如何唤醒Java线程,
在并发的情况又涉及到,如何原子操作修改资源状态
等待的线程是采用双向链表作为队列的,因此涉及到数据结构双向链表的应用
双向链表就涉及到添加节点,删除节点,遍历节点等操作
因此AQS的实现是Java基础知识的一个综合应用,
如果我们能深入的理解,不仅仅是可以应付面试的提问,更重要的是我们可以应用其思想解决我们在实际开中遇到的业务问题;
举一个我个人在实际开发中借鉴AQS思想解决的业务问题,
业务场景是这样的,提供一个接口去查询某个商品是否有货,这个商品由多个供货商供货的,只要我查询到有一个供货商能够供货就表示该商品可以出售的.
最简单的做法,当然循环查询(http请求)每一个供货商,直到找可以供货的商家,就停止查询,很明显这样的查询效率是很低的...
我希望的是同时去查询所有的供货商,只要有一家供货商返回了有货,该接口就可以返回商品可售了
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/java-jiao-cheng/5860.html