Java并发系列-AQS详解
前言
- AQS核心思想是什么?如何实现的,以及底层的数据结构
- 线程获取锁失败,之后的处理流程是什么
- 处于排队等候机制中的线程一直无法获取到锁,需要一直等待嘛,AQS还有用别的策略解决这一问题
- Lock函数通过Acquire方法进行加锁的时候,具体是如何进行加锁
AQS简介
AQS全称为AbstractQueuedSynchronizer,AQS是用来构建锁和同步器的框架,类似我们常用的ReentrantLock,CountDownLatch,ThreadPoolExecutor等,核心思想主要是:用一个volatle修饰的state表示共享资源是否空闲,如果请求的共享资源空闲,会将当前的请求资源的线程设置为有效工作线程,并且会将共享资源设置为锁定状态。如果被请求的共享资源被占用的话,AQS通过CLH队列实现了线程阻塞等待以及唤醒机制,会将暂时获取不到锁的线程加入到队列中。
1 | java复制代码 private volatile int state; |
state
我们可以看到,这个状态量为private,内部提供了几个访问这个字段的方法(final修饰的,子类无法重写)
1 | java复制代码//获取State值 |
独占和共享
通过和state对比,实现独占和共享
1 | java复制代码/** |
自定义的同步器要么实现独占,要么实现共享,我们在重写的时候,只需要实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。接下来我们非公平锁为例子,了解一下队列
队列
多线程来请求共享资源,如果线程获取锁失败(获取锁的方式分为公平和非公平方式),就需要加入等待队列中,带着下面两个问题,我们看下队列相关的知识点
- 获取锁失败的线程何时加入队列
- 如何加入队列
何时加入队列
我们借助ReentrantLock来看下AQS相关队列的东西,平常开发过程中我们使用ReentrantLock的时候,一般是这样使用的
1 | java复制代码 Lock lock = new ReentrantLock(); |
ReentrantLock底层是AQS来实现的,ReentrantLock内部有分为公平锁和非公平锁,
1 | java复制代码//默认是非公平锁 |
从上面源代码中,我们可以总结出第一个问题的答案,当线程执行Acquire(1)尝试获取锁的时候,如果获取失败,调用addWaiter加入等待队列中
如何加入队列
获取锁失败之后,会调用addWaiter方法,将线程封装成Node节点加入队列中,具体实现方法如下
1 | java复制代码//java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter |
如图所示,
- 新节点来的之后,首先将新节点的前驱节点指向尾节点
- 通过cas(pred,node)方法完成尾结点指向新插入的节点这个操作。
- 为啥用CAS方式设置尾部节点,原因就是如果我们在设置途中,如果别的线程比如T2先这个节点插入尾部,这个时候Pred指针指的是老的位置,Tail这个时候应该指向的是新T2线程的Node节点位置,这个时候CAS失败,就需要走enq方法完成尾节点设置。
1 | java复制代码 private Node enq(final Node node) { |
啥时候出队列
这线程以Node节点的方式添加到等待队列中了,那什么时候出去呢,接下来我们带着这两个问题一起了解下啥时候出队列
- 何时出队列(添加节点成功之后 队列中的元素就会一直尝试获取锁)
- 如何出队列(节点获取锁成功,将节点设置为取消状态,节点出队列)
上面我们讲的addWait方法,会将线程以Node节点的数据结构添加到双端队列中,这个方法其实是返回一个包含改线程的Node,Node作为参数进入到acquireQueued()方法。acquireQueued()就是对排队的线程获取锁操作,这玩意会一直把放入队列中的线程不断的获取锁,一直到获取成功或者不再获取(线程被中断)。我们一起看下**acquireQueued()**方法
1 | java复制代码 public final void acquire(int arg) { |
简单总结一下上面的流程:
- CAS自旋的方式判断传入的Node节点的前节点状态,如果是前节点是head节点,Node就尝试获取锁,获取成功的话就将Node节点设置为Head节点,以前的Head等于null,然后跳出自旋,final中不执行
- 如果前节点不是head获取加锁失败,就调用shouldParkAfterFailedAcquire,函数会将前驱节点的waitStatus变为了SIGNAL=-1,调用 parkAndCheckInterrupt会挂起当前线程,Node等待前面节点唤醒。
看完上面的源码,我们可能会想到一下新的问题:
- shouldParkAfterFailedAcquire()中取消节点是什么时候生成的?
- 线程被挂起的时候什么时候被通知()
cancelled节点生成
获取锁失败(什么时候失败?中断,获取锁超时?),则将此线程对应的node的waitStatus改为CANCEL,会在final里面将节点设置为取消状态,总结三种情况
- 要取消的节点是尾节点
- 当前节点如果是尾结点,从后往前设置第一个非取消状态设置为未节点
- 要取消的节点是Head的后继节点
- 唤醒当前节点的后继节点
- 要取消的节点在链表的中间
- 把当前节点的前驱节点的后继指针指向当前节点的后继节点
1 | java复制代码 private void cancelAcquire(Node node) { |
如何解锁
AQS中释放锁的方法是release(),如果state=0(彻底释放)。就会唤醒等待队列中的其他线程来获取资源
1 | java复制代码 //独占模式下释放锁 |
总结
- tryRelease方法会减去state对于的值,等于0的话会彻底释放资源,完全释放资源之后,会调用unparkSuccessor唤醒CLH队列中第一个等待资源的线程
公平锁VS非公平锁
1 | java复制代码 /** |
问题
我们尝试说下这几个问题的回答点
- AQS核心思想是什么?如何实现的,以及底层的数据结构
- state以及CLH队列
- 线程获取锁失败,之后的处理流程是什么
- 队列排队等候,线程继续等待,保留获取锁的可能,获取锁的流程仍然继续
- 处于排队等候机制中的线程一直无法获取到锁,需要一直等待嘛,AQS还有用别的策略解决这一问题
- 节点变为取消状态(finally执行的),节点从链表中摘掉
- Lock函数通过Acquire方法进行加锁的时候,具体是如何进行加锁
- AQS的Acquire会调用tryAcquire方法,tryAcquire由各个自定义同步器实现(公平非公平),通过tryAcquire完成加锁过程,
闲谈
感觉有帮助的同学还请点赞关注,这将对我是很大的鼓励~,公众号有自己开始总结的一系列文章,需要的小伙伴还请关注下个人公众号程序员fly呀,干货多多,湿货也不少(∩_∩)。
巨人肩膀
- 文章主要参考自美团技术文章从ReentrantLock的实现看AQS的原理及应用,在此基础上做了增改
- juejin.cn/post/700689…
- tech.meituan.com/2019/12/05/…
- juejin.cn/post/689627…
本文转载自: 掘金