AQS 全称 AbstractQueuedSynchronizer (抽象的队列同步器)单看名字你可能不知道他干嘛的。但是如果告诉你ReentrantLock(可重入锁)就是基于它来实现的你可能就知道它的作用了。
先来看下AQS的类关系图:
AQS继承自 AbstractOwnableSynchronizer类,AbstractOwnableSynchronizer的实现其实非常简单,就是持有一个线程,提供了相关的getter/setter方法。仅此而已。
AQS继承了exclusiveOwnerThread属性,表示独占锁的线程。
如果想很清楚的AQS是怎么实现同步的,找一个用它来实现锁功能的类来分析源码是再好不过了。我们就拿大名顶顶的的ReentrantLock(可重入锁)来配合分析AQS吧。
ReentrantLock(可重入锁)分公平锁和非公平锁。ReentrantLock 内部维护了两个内部类NonfairSync(非公平同步器)和FairSync(公平同步器)来实现公平锁和非公平锁。ReentrantLock默认情况下是非公平锁。我们先来看看公平锁是怎么实现的:
ReentrantLock的lock()方法直接调用相关同步器的lock()方法。FairSync(公平同步器)的源码很简单,我们来大致看下:
lock方法很简单,就是调用AQS的acquire()方法。acquire顾名思义获取。我们看看它到底是要获取什么。
首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试。因为有可能直接就成功了呢,也就不需要进队列排队了。所以它写的是!tryAcquire(1)。如果tryAcquire(1)没成功,那就说明要排队等锁啦,此时如果线程也排队成功了,那就把线程设置中断。同时返回给要锁的线程false,代表抢锁失败啦。
我们来详细分析下公平同步器FairSync的tryAcquire的实现。
1、getState()获取当前锁的状态,如果是0,则需要判断是不是队列中是不是有线程在等锁,为啥要这么判断一次,因为是公平锁啊。不能说你去请求锁的时候没有人占用锁,你就直接占了,因为可能有人已经排队了。如果队列中没有等待的线程,此时就各凭本事了,那就看谁单身的时间长了,此时就会用发起一个CAS尝试改变锁的state,如果操作成功了,说明抢锁成功,把锁的线程设置成自己,表明自己占有了锁。
2、如果锁已经被占用,那看看是不是自己占有的,因为是可重入锁吗。如果是自己占用的,就把状态继续加1。如果不是,那只能说明尝试抢锁失败了。此时又回重新进到AQS的实现里来进行排队操作。
对于高并发场景下,tryAcquire是一个乐观思想,但大多数情况下肯定还是会进入排队状态。那我们来看看AQS是怎么进行给线程排队的吧。
先大致说下有个概念我们再去看源码。
AQS内定义了如下几个变量来配合实现相关功能。
private transient volatile Node head; 持有当前锁的线程的节点
private transient volatile Node tail; 等待锁的线程的最后一个节点
private volatile int state; 大于0代表锁有被线程占用
这里我们需要具体讲解下Node这个类,Node类是AQS的一个静态内部类。这个类是一个实现锁相关的辅助类。每个Node 实例都会持有一个线程的引用,每个Node可能会有一个前驱Node,和一个后继Node,如果Node没有前驱,它肯定会被AQS的head引用,如果一个Node没有后继,那他肯定会被tail所引用。
拿head这个Node来说吧,这个Node持有的线程会持有锁。当head持有的锁未被释放时,如果还有线程要抢占锁,那只能进入排队,会产生一个新的Node。你可以把Node理解为持有锁的线程的包装。每有一个线程要这个锁。就会形成一个Node,维护在锁内部进行排队。
排队的核心方法是addWaiter(Node mode),你以为它的参数Node就是直接加入一个节点进行排队吗。我告诉你它不是。JDK8以前可能是,但是JDK8不是,JDK8在Node内入引入了一个新变量Node nextWaiter;从官方的注释来看。这个对象是一个已有的Node,如果被传入一个Node中,则说明这两个Node可以共享锁。这个我们暂且不管,我们现在脑子只要记得要排队的Node是在addWaiter内部创建的就好了。此时应该搭配源码食用更佳:
为当前线程线程创建一个Node,判断队列的尾部是不是空,如果不是空就说明已经有线程在排队了,那就使用CAS把自己插到尾部。如果每成功,说明有别的线程比自己先排队了。执行enq方法。
addWaiter方法能看出,要排队的Node插入到尾部失败了,或者第一排队都会走到enq这个方法。这个方法分两种情况:
1、尾节点不是空,通过CAS把自己变成尾部节点。成功就直接返回啦,不成功通过自旋继续尝试直至成功。
2、尾节点是空,说明此时还没有人进入到排队队列中,此时头节点肯定也是空,别问我为啥。那此时就需要初始化一个头节点。另外自己不可能是头节点啦。上面已经说啦,head节点是为了对应持有锁的那个节点。但是head这个节点是第一个进入排队排队的线程负责创建的。但也只是尝试创建,因为也有可能别的线程现在抢着进入排队呢。不管是那个线程把head节点创建好了把,反正head节点有了,因为是刚创建的,所以头节点也是尾节点。此时尾节点也不为空啦,执行情况1.
就这样,AQS就帮你实现线程排队啦。
说完了加锁和排队,我们再来说说解锁:
ReentrantLock的解锁也很简单,也就是调用相关同步其的relese()方法。relese()方法也是AQS实现的,我们来具体看下:
和加锁一样的套路,先尝试解锁,如果没成功解锁,则返回false 。tryRelease()方法是个抽象方法,具体由子类实现。我们来看看ReentrantLock的同步器是怎么实现的:
1、判断要解锁的线程是不是当前持有锁的线程,如果不是抛异常。
2、对锁的状态减1,判断状态是不是0,为啥要判断为不为0,因为是重入锁吗,一个线程可能加锁多次呢。这也是为啥ReentrantLock容易造成死锁的原因,你加了多少此锁,你就要解除多少次,不然别的线程永远拿不到锁。
3、如果是完全解锁了,就把持有该锁的线程置为空。同时告诉AQS的release要干继续干事了。
尝试解锁成功后:
判断头节点是不是空,如果是空,直接返回了,如果不是空,则判断后继节点是不是在等待锁,如果再等待锁,执行unparkSuccessor()方法。
判断节点状态是不是小于0;为啥要这么判断,这里就要解释下Node的几个状态了:
1:代表此线程取消了争抢这个锁
-1:代表后续节点需要被唤醒:
-2:代表线程条件等待,满足条件才会抢锁
-3:读写锁中,当读锁最开始没有获取到操作权限,得到后会发起一个doReleaseShared()动作,内部也是一个循环,当判定后续的节点状态为0时,尝试通过CAS自旋方式将状态修改为这个状态,表示节点可以运行。
0:初始化状态,也代表正在尝试去获取临界资源的线程所对应的Node的状态
这样就可以这样解释了。
1、如果head节点当前waitStatus<0, 将其修改为0,表示要其他线程自己去竞争。
2、如果waitStatus>0唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1).从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的.来唤醒线程。
执行完完成后,这时会返回到acquireQueued(final Node node, int arg)方法,而此方法 是自旋的,就是为了唤醒的线程重新去获取锁,直到异常退出或者执行完为止。