下面我们以ReentrantLock为例了解AQS
ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。
使用ReentrantLock进行同步
ReentrantLock lock = new ReentrantLock(false);//false为非公平锁,true为公平锁
lock.lock() //加锁
lock.unlock() //解锁
ReentrantLock如何实现synchronized不具备的公平与非公平性呢?
在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对该抽象类的部分方法做了实现;并且还定义了两个子类:
1、FairSync 公平锁的实现
2、NonfairSync 非公平锁的实现
这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个ReentrantLock同时具备公平与非公平特性。
AQS定义了两种锁的模式:
- Exclusive-独占,只有一个线程能执行,如ReentrantLock
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
AQS定义两种队列:
- 同步等待队列
- 条件等待队列
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
下面我们针对AQS同步等待队列进行详解:
当抢占资源的线程失败,就会进入到同步等待队列里面(AQS独占模式)
图为独占模式的同步等待队列(CLH队列):在获取锁失败的队列会在AbstractQueuedSynchronizer里面创建一个CLH队列(同步队列 双向链表指针队列基于Node的 pver和next形成双向链表) CLH队列 都是基于Node节点这个和数据结构 同步队列每一个Node节点都有一个前驱指针和后继指针为了构建一个双向指针队列的结构(都会记录前面和后面一个节点是谁) 条件队列为单项链表 有一个nextWaiter,,同时还会head、tail属性代表队列的头和尾,同时每一个node节点里面还会有一个thread属性记录当前线程是哪一个,后面会基于这个线程去unpark()唤醒。
AQS里面CLH队列是对原生的CLH队列进行了变种:原生的CLH队列里面的线程不会丢失cpu的使用权,而是在队列里面自旋 而AQS的CLH队列是原生CLH队列的变种 也不会丢失cpu使用权,但是队列里面的线程会阻塞。
数据结构是:都是基于Node内部类定义了是共享模式 还是独占模式 同时node节点里面定义线程处于什么状态(状态基于waitStatus信号量) 是否可以被剔除或者唤醒 每一个Node节点都有一个前驱指针和后继指针为了构建一个双向指针队列的结构(都会记录前面和后面一个节点是谁)(双向链表队列)
下面为AQS里面主要方法注释
在简单说源码之前需要提到几个概念:
waitStatus:标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态。节点被唤醒还是被剔除基本上都是依靠这个信号量去判断。
CAS(比较与交换):因为在CAS里面在对于信号量的转变的过程中用到大量的CAS原子算法。CAS原理:相当于通过比较,如果相同就修改,否则就不修改,如果必须要修改,再次读取的一份最新的数据,在最新的数据上面做修改,这个是一个原子操作,底层依赖于unsafe类,提供了3个比较与交换的api(对象,Long,Int) 都是通过C++写的
下面为主要源码细节:
获取锁的调用accquireQueued中的tryAccquire()方法 判断前驱节点是否为Head头节点 如果是将当前节点置为头节点 将next置为null, 如果不是head节点判断该节点是否有效是否一个被阻塞 如果前驱节点为信号量(waitStatus)-1 则阻塞
1,关于锁竞争加锁和加锁失败入队的的逻辑tryAcquire(arg)都在这个方法里面
当尝试去获取锁的时候先会去判断state是否等于0 同时也会判断队列的头和尾是否相同 如果state=0 并且队列为空 会利用CAS算法修改state的值,同时将线程的引用exclusiveOwnerThread属性改为当前线程。 如果state不为0,判断锁是否为自己持有(根据exclusiveOwnerThread是否等于自己(利用了可重入锁)),如果是自己state直接加1,如果不是直接返回加锁失败,就会入队列里面。
2,获取锁失败入队的逻辑 acquireQueued(addwirte(Node.Exclusive)) 线程排队的逻辑
Node节点里面可以判断是互赤的还是共享的 判断CLH队列里面有没有等待的线程 如果tail不为空 说明队列里面已经有等待的线程 创建新的节点node(node节点里面有pre next waitstatus thread四个比较重要的变量) 先将前驱节点(prev)指向前一个node, thread=当前线程 同时将前一个node节点的next指向新的node节点 再将tail指向新的node节点 在通过CAS操作将这个节点插入CLH队列里面 返回新建的这个节点 ) 如果tail为空 创建一个初始的节点node 将head和tail都指向这个node 当有队列来排队 将新入的队列prev为null 而再通过CAS操作将 tail指向新的node 执行一个循环一遍一遍尝试直到成功为止 主要是waitstatus(表示当前节点的生命状态也称为信号量总共有四个值)的改变去判断当前线程是否可以被唤醒, 还有当出现节点状态为1的时候,就会剔除CLH队列
当线程释放锁还会唤醒后面的节点去拿锁(LookSport.unPark()方法)
入队检查:
当新的节点尝试插入CLH队列尾部的时候 用去对CLH队列里面的node做一个全面检查, 判断要插入前一个节点是否为无效的节点(status信号量为cancelled=1) 如果是无效的节点直接剔除
说到这里可能很多人会有疑问,为什么很多循环而不会出现问题的原因?
阻塞,因为获取线程的方法为死循环, 所以当进入后续循环的时候将此线程调用AQS底层方法Unsafe.park()方法阻塞,并且返回,此循环就已经停止, 这也是为什么写这么多循环不会出问题的原因。