目录
一 简介
AQS(AbstractQueuedSynchronizer)是java并发工具的基础,底层很多实现都采用了CAS乐观锁的方式,在锁获取冲突时,通过自旋重试来轻量的获取锁(synchronized的偏向锁、轻量级锁和重量级锁也有CAS操作,我理解目的都是尽可能的减少获取锁带来的上下文开销)。
AQS是一个抽象类,但是没有抽象方法。这样子类可以根据自己的需要实现对象的方法,而不用实现所有的方法,未实现的方法会默认抛出UnsupportedOperationException
异常。图1.1列出了AbstractQueuedSynchronizer的相关方法:
图 1.1
AQS继承关系如图所示:
图 1.2
二 AQS三大核心概念
synchronization state
synchronization state为同步状态 ,由全局共享,很多操作会依赖于这个状态。state被设置为volatile类型,保证了其可见性。state大于0表示当前已经有线程持有锁,AQS作者在设计的时候考虑到重入的情况,state的具体正整数值表示当前的重入次数。
图 2.1
通过state能够知道锁被线程持有,但是无法知道被哪个线程持有,AQS通过exclusiveOwnerThread这个变量来记录是哪个线程持有锁,exclusiveOwnerThread变量在AbstractQueuedSynchronizer的父类AbstractOwnableSynchronizer中。
图 2.2
Node 队列
AQS有一个同步队列,这个同步队列里面的结构是一个Node对象,这个队列存储的是等待获取锁的线程。Node类的相关属性如下,直接复制的源码:
/** <p>To enqueue into a CLH lock, you atomically splice it in as new
* tail. To dequeue, you just set the head field.
* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>
*/
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
/**
* Link to predecessor node that current node/thread relies on
* for checking waitStatus. Assigned during enqueuing, and nulled
* out (for sake of GC) only upon dequeuing. Also, upon
* cancellation of a predecessor, we short-circuit while
* finding a non-cancelled one, which will always exist
* because the head node is never cancelled: A node becomes
* head only as a result of successful acquire. A
* cancelled thread never succeeds in acquiring, and a thread only
* cancels itself, not any other node.
*/
volatile Node prev;
/**
* Link to the successor node that the current node/thread
* unparks upon release. Assigned during enqueuing, adjusted
* when bypassing cancelled predecessors, and nulled out (for
* sake of GC) when dequeued. The enq operation does not
* assign next field of a predecessor until after attachment,
* so seeing a null next field does not necessarily mean that
* node is at end of queue. However, if a next field appears
* to be null, we can scan prev's from the tail to
* double-check. The next field of cancelled nodes is set to
* point to the node itself instead of null, to make life
* easier for isOnSyncQueue.
*/
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED. Because condition queues are accessed only
* when holding in exclusive mode, we just need a simple
* linked queue to hold nodes while they are waiting on
* conditions. They are then transferred to the queue to
* re-acquire. And because conditions can only be exclusive,
* we save a field by using special value to indicate shared
* mode.
*/
Node nextWaiter;
waitStatus:表示节点所处的状态,初始为0,可能的取值有CANCELLED = 1,SIGNAL = -1, CONDITION = -2, PROPAGATE = -3。
thread:当前Node代表的线程
pre:node前驱
next:node后继
nextWaiter:独占锁模式下,该值为null
特别说明下,从源码中官方说明知道,这个同步队列为一个CLH队列。这个队列永远有一个不代表任何线程的dump node(有情况还是会代表线程的,只是此时它的thread为null,后续会讲),处于队首。当有线程没抢到锁被包装成Node节点放入队列时,如果此时队列为空,会先创建一个dump node,再把这个node节点放到dump node的后面。同步队列示意图如图所示:
图 2.3
CAS
JDK作者为了提高效率,在AQS中,很多状态的改变采用了CAS的方式来改变。如下所示:
/**
* CAS head field. Used only by enq.
*/
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
/**
* CAS tail field. Used only by enq.
*/
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
/**
* CAS waitStatus field of a node.
*/
private static final boolean compareAndSetWaitStatus(Node node,
int expect,
int update) {
return unsafe.compareAndSwapInt(node, waitStatusOffset,
expect, update);
}
/**
* CAS next field of a node.
*/
private static final boolean compareAndSetNext(Node node,
Node expect,
Node update) {
return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}
三 从ReentranLock走进源码
ReentranLock的大体结构如图所示:
图 3.1 ReentranLock大体结构
ReentranLock有公平锁和非公平锁两种,默认是非公平锁,创建ReentranLock时通过传入ture new ReentrantLock(true) 就能创建公平锁。以公平锁为例,通过一个demo的方式来debug跟踪源码(该demo网上找的一个示例,这里目的就是为了debug看看具体的流程而言)。demo如下
图3.2 demo
tryAcquire
先分析获取锁的流程,在tryAcquire里面打个断点,看下调用栈,如图3.3
图 3.3
先回到上一步的acquire方法简要说一下:
图 3.4
在acquire方法中涉及到了4个方法,分别作用为:
(1)tryAcquire(arg):获取锁的具体逻辑,获取锁成功返回true,失败为fasle。
(2) addWaiter(Node mode): 获取锁失败后,将当前获取锁的线程包装成Node节点放入同步队列中;
(3)acquireQueued(final Node node, int arg):尝试获取锁或挂起线程,不是都获取锁失败了吗,这个为什么还会尝试获取锁呢,下面会讲
(4)selfInterrupt:这个方法的作用可以理解为记录中断,在整个抢锁过程中是不会响应中断的,如果发生了中断,怎么办呢?AQS在抢锁过程中会记录是否发生中断,发生了中断,就调用selfInterrupt()自我中断下,有点感觉像是将抢锁中发生的中断推迟到了抢锁结束后。
回到tryAcquire()方法,如下:
图 3.5
通过getState()获取当前状态,如果为0表示当前没有线程占有锁,会进入下一步if的判断。这里是公平锁,所以会先调用hasQueuedPredecessors方法来看之前有没有其他线程在等待锁,如果没有,通过CAS获取到锁后就返回ture了。hasQueuedPredecessors方法代码如下:
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
代码逻辑很简单,就是判断我是不是head节点的后继节点,如果是就表明我是第一顺位,如果不是就说明有其他线程排在我前面在等待获取锁。
这里假设我是第一顺位,就会通过CAS获取锁。CAS执行的操作是修改state的状态,如果不是重入的,这里会将state的状态由0变为1,如果是重入的,会修改为重入的次数。CAS操作必然只有一个线程会最终成功,成功获取到锁的线程会调用setExclusiveOwnerThread方发将当前锁拥有的线程设置为自己。
图 3.6
如果上诉获取的c大于0,说明有线程占有了锁,会走到else if逻辑,在这个逻辑里会先通过
current == getExclusiveOwnerThread()检查是不是就是我自己,如果是就直接进行锁重入,调用setState(next)改变state的值。
图 3.7
如果也不是重入锁的话,那就说明抢占锁失败,返回false。这里有一点需要说明下,当c=0获取锁的是否是通过CAS操作对state设值来保证线程安全,但当进入重入逻辑给state设置的时候,是直接调用的setState(next),没有用CAS也没有加锁这些,那么这一步操作线程安全吗?答案是安全的,因为进入到重入的逻辑,说明这个锁已经被持有了(我自己),其他线程是无法进入的。
抢占锁失败后,会进入到
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt()
中的addWaiter方法。
addWaiter
代码如下:
图 3.8
由于是独占锁,addWaiter传入的是Node.EXCLUSIVE,该值就为null, 从Node的的构造方法可知:
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
独占锁模式下的nextWaiter恒为null。在addWaiter的逻辑很简单,就是通过CAS将当前节点设置为同步队列的尾节点。注意,由于这里会有多个线程会执行设置尾节点的操作,因此失败的线程会进入到end(node)逻辑。还有一种情况,当pred为null,说明队列为空的时候,也会走到end(node)逻辑。
end
队列为空或队列不为空但加入同步队列失败后,进入到 end(node)逻辑。代码如下:
图 3.9
如果队列为空,会调用CAS初始化节点,注意这里是通过new Node()创建了头结点,这个头结点不代表任何线程,就是之前说的dump node。在创建好dump node后,再将当前节点链接到到dump node后。
当队列不为空的时候,会执行CAS操作将当前节点连接到队列尾巴后,注意看一下代码,有个贼神奇的事情。
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
这段代码做了三件事情:
1.设置node的前驱节点为当前同步队列的尾节点;
2.通过CAS操作将当前尾节点设置为node节点;
3.将老的尾节点的next指向node节点。
这三步一看就不是一个原子操作,因此当有大量线程执行到这里的时候,只有一个线程能够完成上诉3个步骤,其他线程执行完成第一步。会出现一个现象,通过下图来说明:
图 3.10
从图3.10可知,CAS未成功的Node节点是不会和之前的尾节点关联上的,当有大量线程去执行上诉操作的时候就会出现图3.10的样子。
特别说明,在CAS源码中,在遍历同步队列的时候都是从尾节点开始遍历的,这也是由上诉那三部决定的。如果,当执行完第二步完毕,第三步还没执行完毕的时候,即旧的尾节点的next为null, 要是此时恰好有线程从头遍历队列的话,是遍历不到这个新的尾节点的。如果从尾节点开始遍历,这个时候是能够遍历到所有节点的。因为此时新的尾节点的pre已经指向了旧的尾节点。
这个for(;;)循环的作用就是会将那些加入队列失败的Node节点,一直会循序加入到队列中,直到成功为止,当所有Node节点都入队成功后,循环终止。如图表示不断循环直到入队成功的中间阶段示意图:
图 3.11
acquireQueued
addWaiter方法结束后,会进入到acquireQueued方法。先提前声明下,这个方法里面有个神奇的出队操作(我理解是出队),就是如果当前入队的是第一顺位的节点,会将该节点设置为新的dump 节点。还有,在这个方法里有可能还会执行一次抢锁操作,当当前节点的前驱节点是HEAD节点,此时这个节点head节点是dump node,不代表任何线程(thread 为null理解更准确)。说明我当前是第一顺位,所以会再次执行获取锁的操作。acquireQueued代码如下:
图 3.12
可以看到,当当前节点node的前驱节点p是头节点时(这个p是个dump节点),会在进行一个尝试获取锁的操作。如果获取锁成功,进入setHead看看会发现新的dump节点诞生了:
/**
* Sets head of queue to be node, thus dequeuing. Called only by
* acquire methods. Also nulls out unused fields for sake of GC
* and to suppress unnecessary signals and traversals.
*
* @param node the node
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
setHead方法将新的头节点设置为当前节点,同时将thread和pre设置为null,这个操作和最开始初始化队列通过new Node创建的dump节点是一样的。因此,以前的dump节点相当于出队了,新的dump节点诞生了。这里可能有人会有疑问,就是这里把thread设置为null,那么怎么知道当前获取锁的线程是哪个呢?别忘了,在获取锁成功是已经设置一个变量exclusiveOwnerThread来记录当前获取锁的线程。这里thread其实已经没用了。新的dump 节点诞生,这个时候的dump 节点可以理解为是当前持有锁的线程节点。
源码中在将老的dump节点出队时,执行了p.next = null的操作,目的是为了防止内存泄漏,触发gc的时候能够将老的dump node回收掉,源码中也有官方注释,真的处处是细节,太可怕了
图 3.13
这里的setHead也没有通过CAS或者同步处理,因为是在当前节点获取到锁后执行的操作,所以这里一定是线程安全的,不会有其他线程进来。
shouldParkAfterFailedAcquire
当前节点不是第一顺位(没有机会再次获取锁,直接失败),或是第一顺位但获取锁失败后会进入到该方法,来判断是否需要将线程挂起。代码如下:
图 3.14
waitStatus终于派上用场了,先简单说下waitStatus = SIGNAL这个状态。SIGNAL不是表示当前节点的状态,它是表示下一个节点的状态,一个节点的waitStatus被设置SIGNAL是由它的下一个节点设置的,作用是当当前节点释放锁后,它通过SIGNAL来唤醒下一个节点。当一个线程挂起前,它会给它的前驱节点的waitStatus被设置SIGNAL。
这段代码,首先是获取到前驱节点的状态,如果前驱节点状态为SIGNAL。说明当前节点已将让前驱节点在释放锁的时候通知我了,直接返回ture。
当ws > 0,只有ws = CANCELLED = 1是大于0的,这里只能为CANCELLED态。开头有讲。
为CANCELLED态说明前驱节点已经由于超时啊,中断啊等原因放弃了等待锁。因为当前节点需要找到能把自己唤醒的节点啊,所以会继续向前找,直到找到需要等待锁的节点,并将当前节点放在该节点的后面。
图 3.15
如果前驱节点的状态既不是SIGNAL也不是CANCELLED,说明这个是把我唤醒的节点,那么给这个节点的waitStatus被设置SIGNAL。
图 3.16
shouldParkAfterFailedAcquire返回fasle的时候,会退到acquireQueued方法,acquireQueued里面是一个for(;;)死循环,当前节点会继续回到循环中尝试获取锁,反正就一直循环,直到获取锁成功,或获取找到能够唤醒我的前驱节点为止。然后进入parkAndCheckInterrupt方法。
parkAndCheckInterrupt
parkAndCheckInterrupt代码如下:
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
到这里,通过LockSupport.park将当前线程挂起,当前线程后面的代码不会执行了,除非其他线程unpark了当前线程。
AQS独占锁源码简单分析到此结束,这里感谢https://segmentfault.com/u/chiucheng的博客给了我很多启发。
四 补充
补充下非公平锁的获取,其实逻辑很简单,从源码可知,非公平锁一进来就会尝试获取锁,获取不到才会执行acquire 。
图 4.1