浅谈AQS同步队列(含ReentrantLock加锁和解锁源码分析)
Posted 默辨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈AQS同步队列(含ReentrantLock加锁和解锁源码分析)相关的知识,希望对你有一定的参考价值。
文章目录
一、相关理论
这里涉及的理论与synchronized关键字相似 要是面试官再问我synchronized,我就这么答
无论我们在程序中使用那种锁,其背后的理论基础都是基于操作系统的管程概念。管程不断发展,先后出现各种管程模型,无论是AQS还是synchronized都是基于MESA模型。
MESA模型:需要有入口等待队列(同步队列)起到互斥作用,多个条件队列起到线程数据同步、线程唤醒的作用。
在我文章口头有给出synchronized关键字的相关讲解说明。synchronized关键字是基于JVM层面,即在JVM层面完成了对管程思想的实现。
AQS则是基于Java层面,即在Java代码层面完成对管程思想的实现。
synchronized升级为重量级锁后,锁定资源是操作系统将会又用户态转变为内核态,这是十分消耗性能的操作。AQS则更所的是基于用户态的资源锁定。两者各有优略(synchronized有锁升级过程,aqs定制化更高),毋庸置疑的是,两者都用于解决线程之间的并发安全的问题
二、执行原理
1、同步等待队列和条件等待队列
同步等待队列
类比理解synchronized中的ContentionList和EntryList队列
AQS中的同步队列也称CLH队列,CLH队列是由Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
AQS 依赖CLH同步队列来完成同步状态的管理:
- 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程
- 当同步状态释放时,会把首节点的next节点唤醒(公平锁),使其再次尝试获取同步状态。
- 通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)
条件等待队列
类比理解synchronized中的WaitSet队列
AQS中条件队列是使用单向列表保存的,用nextWaiter来连接:
- 调用await方法阻塞线程;
- 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)
2、补充
AQS具备的特性
- 阻塞等待队列:条件队列和同步队列
- 共享/独占:在ReentrantLock中实现时,是独占锁(对应的state变量表示加锁状态);在Semaphore、CountDownLatch中是共享锁(对应的state变量表示共享的线程数量)
- 公平/非公平:可以自己指定参数来决定是公平锁还是非公平锁,默认是非公平的
- 可重入:在ReentrantLock中实现时,可以锁里面继续套锁(对应的state变量+1)
- 允许中断:可以手动中断线程
ReentrantLock是一个经典的AQS实现
synchronized和ReentrantLock的区别:
- synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
- synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
- synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
- synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;
- 在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显示释放锁;
- ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
- synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;
三、同步队列相关流程及源码
AQS只是一个抽象类,其本质只是对管程模型的一种思想实现,它完成了对象在不同队列之间的转移及状态变化。在这个基本框架的基础上,我们可以衍生出很多的锁,甚至是锁的衍生品。后面的流程分析是基于ReentrantLock讲解。
1、lock.lock()
以非公平为例
首先会尝试获取锁,获取到了锁。就将当前线程绑定为锁的拥有者。没有获取到锁就进行入队
入队操作主要是下面的三个方法:tryAcquire、addWaiter、acquireQueued
2、tryAcquire
3、addWaiter
enq方法代码。该方法包含了两个作用:
1、初始化一个含有null节点的队列
2、将节点添加到已存在的队列的末尾
你可能有疑惑,在addWaiter方法中已经存在了将节点添加到队尾的方法,为什么enq方法中还有该功能呢?同理,addWaiter方法中已经判断队列是否为空,为什么enq方法中再次判断?
不得不多,Doug Lea写代码真的是又简洁,包含的细节还多。在并发场景下,很有可能第一个线程首先判断队列为null,那么它就会调用enq方法进行创建队列,与此同时,另一个线程也执行到了这里,并且抢先一步完成了队列的创建工作,按照我们的传统想法,此时就会又创建出一个队列,这是有问题的。
在Doug Lea的代码下,enq方法中又进行了一次判断,首先进入该方法的线程进行CAS操作必然成功,第二个必然失败,失败的线程又会再次循环,此时执行的就是else中的方法,将该节点添加到队列的末尾,第一次执行CAS操作的线程也会再次进行for循环,将当前node节点添加到队列后面。(所以此时队列中有三个节点,一个空节点、一个线程1的节点、一个线程2的节点)
4、acquireQueued
addWaiter方法返回的是入队的Node节点,然后将入队的Node节点进行阻塞
该方法分为两部分理解:
第一部分:如果当前节点的前一个节点是队列的头节点(只有两个节点),并且当前节点(当前线程)又能获取到锁。这就表示,代表的之前获取到锁资源的线程释放了锁资源,不然当前Node也不会获取到锁。锁资源释放(state=0),我们可以理即为之前的线程已经结束,要开始新的线程任务,把当前节点设置为头节点,然后移除头节点(初始时默认的空节点)。
第二部分:如果当前节点前面的节点不是头节点(队列中有很多线程Node节点),那么就会将它前一个节点的waitStatus状态修改为-1,然后调用parkAndCheckInterrupt方法进行阻塞。想想此时队列中的线程Node节点的状态,除了最后一个Node节点,前面的Node节点的waitStatus状态都变成了-1
第二部分的代码最终都会跳转到第一部分的代码逻辑,相当于队列的头节点一点一点的被截断。
将当前Node节点的前一个节点的waitStatus状态设置为-1方法
调用park方法,阻塞线程
经过前面的四部,线程就已经被添加到了队列中,并且成功的阻塞住了。接下来就是解锁逻辑
5、lock.unlock()
调用unlock方法,会进入到release方法。head节点是一个空节点
该方法也主要分为两部分:
第一部分:释放当前线程对锁的资源
第二部分:唤醒下一个线程
释放当前线程对锁的资源
唤醒下一个线程
重点
这里的唤醒下一个线程的操作需要和前面加锁的逻辑一起看才好理解(我反正最开始没有想明白)
试想一下,现在有三个线程开始执行,第一个线程获取到锁,第二个、第三个线程分别封装为Node节点,然后添加到队列中,队列的形式为 Node1(null) --> Node2(Thread2) --> Node3(Thread3),并且Node2和Node3调用了park方法,都阻塞在队列中(Node1和Node2的waitStatus都是-1)。
拥有锁资源的线程调用unlock方法,释放锁资源。在唤醒其他线程资源的时候,会先判断头节点的waitStatus不等于0,然后修改头节点的waitStatus为0,再调用对应的unpark方法,唤醒头节点的下一个节点即Node2。此时看右边的代码,Node2停止阻塞,再次进行for循环,Node2的前节点是头节点,并且已经释放锁,所以能够进入该方法,然后把Node设置为头节点,再移除Node1节点。
四、条件队列
该部分的知识在CyclicBarrier这个工具类中,类似于栅栏的作用。
该部分代码的逻辑与同步队列的代码逻辑重合度很高(第三节),可以说比同步队列的更简单,毕竟同步队列还是一个双向链表的结构的队列,而条件队列仅仅是一个单向链表结构的队列。
区别于同步队列,条件队列中的每一个线程也是封装成一个Node节点,只是Node节点里面的waitStatus的值为-2(同步队列的waitStatus值为-1)。
以上是关于浅谈AQS同步队列(含ReentrantLock加锁和解锁源码分析)的主要内容,如果未能解决你的问题,请参考以下文章
面试官:从源码角度讲讲ReentrantLock及队列同步器(AQS)