本来是想要看AQS的源码的,然后上网上找了好多,最后还是从ReentrantLock开始一点点看起的~ 今天是周日,今天也要加油鸭,本文有很多参考别人博客的地方,但是也有自己的整理哟~
一、AQS原理
AQS的全称为:AbstractQueuedSynchronizer
,它是在java.util.concurrent.locks
下面的类。该类是同步工具的一个基础类,很多类都是通过创建自己的内部类Sync然后继承AQS来实现的。首先,我们看看这个类的一些比较重要的方法。
- 使用了一个int成员变量表示同步状态
- 通过内置的FIFO队列来完成资源获取线程的排队工作
上面画出的方法是与获取锁有直接关系的方法,当然该类中还有一些比较重要的其他方法,这里没有画出来,比如:getState()
等。
下面的图是继承AQS类的一些类
图片作者:谢宇
1.1 举个栗子
为了更好的理解AQS,我们先举个栗子
我们去医院看医生,挂号在某个特定教授。如果当前该教授没有任何病人在看病(相当于tryAcquire
),那么我们不需要排队就可以进去看病。否则的话,我们要在门口排队(相当于addWaiter
),等待当前看病的病人出来,出来一个(相当于release
),则正在队列头部的病人可以进去看病了(大概相当于acquireQueued
)。
1.2 AQS的作用
AQS是由一个同步队列(FIFO双向队列)来管理同步状态的,如果线程获取同步状态失效,AQS会将当前线程以及等待装填信息构成一个结点Node加入到同步队列中,同时阻塞当前线程。当同步状态释放的时候,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
AQS中有一个内部类Node,每个Node结点都是一个自旋锁,在阻塞时不断循环读取状态变量,当前驱结点释放同步对象使用权后,跳出循环,执行同步代码。
二、ReentrantLock
锁
在很多锁中都实现了AQS类,而单纯的讲AQS类的连贯性不是很好,所以这里我们从ReentrantLock开始来一步一步看源码。
首先我们看一下ReentrantLock的类结构。
圈出来的方法是ReentrantLock中与获取非公平锁有关的方法
圈出来的方法是AQS类中与获取非公平锁有关的方法
2.1 ReentrantLock源码的独占式获取同步状态(非公平锁)
好了,终于可以开始看源码了!!!
我们先从ReentrantLock的构造方法开始看起,他的构造方法有两个,一个是按照公平锁的方式构建,一个是按照非公平锁的方式构建。
我们先来看非公平锁的方式,可以看到他创建了一个类:NonfairSync
类,这个类是什么呢?这个类是ReentrantLock的内部类,NonfairSync
类继承了ReentrantLock的内部类Sync,Sync又继承了AQS类。他的结构就是下面这个样子的!
那我们现在看看NonfairSync类的lock方法是如何实现的吧。
- 实现方式:
- lock()方法先通过CAS尝试将状态从0修改为1。若直接修改成功,前提条件自然是锁的状态为0,则直接将线程的OWNER修改为当前线程
- 若上一个动作未成功,则会间接调用了acquire(1)来继续操作,这个acquire(int)方法就是在AQS当中。
所以这个lock方法要做什么呢??
就是通过CAS方式来获取锁,如果获取成功就成功,如果获取失败就调用acquire来进行下面的操作!!!
-----------------------------------------------------------------------分割线------------------------------------------------------------------
那我们就接着看AQS中的acquire
方法
【注意:】acquire
方法对中断不敏感,即由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。
-
tryAcquire
方法- 功能: 首先尝试获取锁,看看当前临界区是否有锁锁住,如果有看是不是自己锁住了,如果可以得到锁返回true,否则返回false
- AQS中的该方法是抛出异常,所以找到子类的该方法;
- 子类该方法又调用了Sync类中的nonFairTryAcquire实现该功能
-
addWaiter
方法- 功能:该方法的主要作用就是根据当前线程创建一个node对象,并追加到队列的末端,到达这个方法就说明线程第一次尝试拿锁失败
- 该方法调用的
enq
方法:enq()
方法主要是初始化头结点和末端结点,并将新的结点追加到末端结点并更新末端结点
-
acquireQueued
方法- 功能:自旋。从等待队列中拿到第一个结点,然后使用该节点去获取锁,如果获取成功就返回true,否则将该线程挂起。
-----------------------------------------------------------------------分割线------------------------------------------------------------------
OK 接下来,我们继续来啃这块acquire老骨头,一个一个看,首先看tryAcquire
方法是如何实现的
在AQS类中,tryAcquire
方法是一个抛出异常的方法,所以我们只能看他的实现类中对该方法的实现了。他的实现类:NonfairSync
实现类中,该方法是调用Sync
类中的nonfairTryAcquire
方法。
调用来调用去,我们要知道这个tryAcquire
到底要做的是什么???
他要做的就是:当前线程去尝试获取锁!!!
步骤:
- 首先获取这个锁的状态,如果状态为0,则尝试设置状态为传入的参数(这里传入的是1),若设置成功就代表自己获取到了锁,返回true。状态为设置1的动作在刚开始调用lock操作的时候就判断过一次,所以我们可以看出来,当前线程在获取锁的过程中,其实是尝试了三次获取锁的!!!
- 如果状态不是0,则判断当前线程是否为排它锁的Owner,如果是Owner则尝试将状态增加acquires(这里是增加1),如果这个状态值越界,则会抛出异常提示,如果没有越界,将状态设置进去后返回true。
- 如果状态不是0,且自身不是Owner,则返回false。
-----------------------------------------------------------------------分割线------------------------------------------------------------------
看完了tryAcquire,我们继续看addWaiter
。上源码!
这个addWaiter
到底要做的是什么???
该方法的主要作用是根据当前线程创建一个node结点,将node结点加入到等待队列中,因为到达这个方法的时候,说明该线程没有抢到锁,所以把该线程和该线程的状态来新建一个node结点加到等待队列尾部
【注意:】这个方法是没有锁的,但是他是线程安全的,那么问题来了,他是怎么保证不加锁的前提下实现线程安全的呢?那就是,使用了CAS呀!!!
首先在该方法中,创建了一个Node对象,将当前线程和传入的Node.EXCLUSIVE传入,即Node结点理论上包含了这两项信息。代码中的tail,在第一个结点进来的时候一定是null,所以不会进入第一层的if
判断区域,而是直接进入了end
方法,所以我们先来看enq
方法
这个enq
到底要做的是什么???
该方法的主要作用是将新进来的node结点加入到等待队列中
那么它是怎么加入的呢?
如果当前节点是等待队列中的第一个结点,那就比较好办了,这个时候可能会有多个线程同时去进行判断,但是只能有一个线程成功完成CAS操作,剩下的线程只能走else语句,那么在else语句中,是怎么回事儿呢?看图儿~~
首先进行的线程节点都将自己的前驱结点连接到等待队列的队尾结点上,然后通过CAS操作只有一个线程执行成功,然后返回当前线程节点的前一个结点。
enq方法通过“死循环”来保证结点的正确添加,在“死循环”中通过CAS将结点设置成为尾结点后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。
到这里addWaiter也说完了,那我们就返回到图2-3中。返回到了AQS的acquire方法中。
-----------------------------------------------------------------------分割线------------------------------------------------------------------
结点进入同步队列后,就进入了一个自旋的过程,每个节点都在自省的观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中。
-----------------------------------------------------------------------分割线------------------------------------------------------------------
独占式同步状态获取与释放总结:
- 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;
- 移出队列的条件是前驱节点为头结点且成功获取了同步状态。
- 在释放同步状态时,同步器调用
tryRelease
方法释放同步状态,然后唤醒头结点的后继结点。
-----------------------------------------------------------------------分割线------------------------------------------------------------------
2.2 ReentrantLock源码的共享式获取同步状态
在acquireShared
方法中,AQS
调用tryAcquireShared
方法尝试获取同步状态,tryAcquireShared
返回值为int类型,当返回值大于0,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件是tryAcquireShared
的返回值大于等于0。 注意tryAcquireShared
在AQS中是抛出异常,所以他需要调用子类重写的tryAcquireShared
方法。在这里需要找到实现这个方法的子类,可以查看Semaphore
类的该方法
-----------------------------------------------------------------------分割线------------------------------------------------------------------
具体他是怎么实现的在这里就先不说了,到时候分析Semaphore的时候再说吧。。
-----------------------------------------------------------------------分割线------------------------------------------------------------------
该方法在释放同步状态后,将会唤醒后续处于等待状态的结点。对于能够支持多个线程同时访问的并发组件(如Semaphore),它和独占式主要区别在与tryReleaseShared方法必须保持同步状态(或者线程数)安全释放。一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
2.3 ReentrantLock源码的独占式获取同步状态(公平锁)
hasQueuedPredecessors()
方法用来判断当前线程前面还有没有未获取资源的线程。
剩下的过程与非公平的方式获取锁的源码都是一样的,在此就不再赘述了。。。