AQS是什么
AbstractQueuedSynchronizer(AQS),抽象队列同步器,并发编程的核心,框架。AQS相当于一个模板,具体实现由子类完成,例如ReentrantLock、CountDownLatch、Semaphore都是基于AQS实现的。ReentrantLock和Semaphore都在内部实现了一个AbstractQueuedSynchronizer的子类Sync。
几个重要概念
AQS内部维护了以下几个变量
同步队列
一个FIFO的队列,它是一个双向链表,用来存放因lock()操作进入等待的线程。
Node节点
同步队列存储的元素称为Node,该节点包含节点状态、独占还是共享模式以及前驱后继节点等信息。
节点状态(waitStatus)分别为:
- CANCELLED(取消)=1:表示线程取消了等待。如果取得锁的过程中发生了一些异常,则可能出现取消的情况,比如等待过程中出现了中断异常或者出现timeout。
- SIGNAL(信号)= -1:表示当前节点的后续节点需要被唤醒。
- CONDITION(状态)= -2:线程等待在条件变量队列中。
- PROPAGATE(传播)= -3:引入这个状态是为了解决共享锁并发释放引起线程挂起的bug。
- 0: 初始状态
解锁时,需要判断节点状态是否<0,小于才需要被唤醒。
状态变量state
state就相当于计数器,当state=0,说明没有线程持有锁,资源可访问。当state=1,说明有线程持有锁,当state大于1,说明有重入。
条件变量等待队列
为了维护等待在条件变量上的等待线程,AbstractQueuedSynchronizer 又需要再维护一个条件变量等待队列,也就是那些由 Condition.await() 引起阻塞的线程,等待队列可以有多个。
当 condition 条件满足时,线程会从条件变量等待队列重新进入同步队列进行获取锁的竞争。
源码分析ReentrantLock加锁解锁过程
ReentrantLock lock = new ReentrantLock();
ReentrantLock的构造器可以传入一个boolean参数,true表示公平锁,false表示非公平锁,默认为false。
PS:我们以独占、非公平、无条件模式为例
加锁
首先上锁:lock.lock();
同以下代码可以看到,该方法调用了Sync类下的lock()方法。
进入lock()后,发现Sync类是ReentrantLock中的一个抽象静态内部类,没有实现lock()方法。
因为以非公平锁为例,于是转到NonfairSync(继承Sync,也是在ReentrantLock类中)中继续探究lock()方法的具体实现,通过源码可以知道,lock()方法通过一个cas操作尝试获取锁。
- 若cas成功则执行同步代码。
- 若cas失败,执行acquire()方法。
PS:这里是第一次cas获取锁。
接下来,我们看一下这个acquire()方法,点进去之后发现是AQS中的一个方法,这里的参数arg就是状态变量state。state=0表示资源可以访问。
此方法包含了三个重要方法
- tryAcquire()
- addWaiter()
- acquireQueued()
先来研究下tryAcquire()方法,进入之后发现没有实现,那应该是子类中有具体实现,我们跳到子类ReentrantLock中查看这个方法。
果然,内部调用了nonfairTryAcquire()方法,我们进入此方法
nonfairTryAcquire(),通过getState()获取状态值state。
若state=0
,意味着没有线程持有锁(可重入情况下会>1),又执行了一个cas操作将state置为1(根据传递的参数arg=1),并将当前线程设置为独占线程。
若state!=0
,判断当前线程是否是持有锁的线程(可重入),将锁计数+1,判断是否超过最大数并抛错误。
PS:这里是第二次cas获取锁
于是,我们知道了tryAcquire()方法大致就是内部通过cas操作尝试获取锁。我们继续看下一个方法 addWaiter()。
addWaiter()将当前线程当做一个节点node,并试图加入到同步队列中的队尾。如果队尾元素为空,通过 enq(node) 初始化队列,并添加元素。
PS:enq(node) 内部也是通过cas操作实现,有兴趣的读者可以看源码。
最后就是 acquireQueued() 方法,它是存储在队列中的线程尝试获取锁的方法,内部也是调用了一个tryAcquire()方法,此方法之前也是提到过,通过cas获取锁。
这里 if 有两个判断条件:
- 当前线程的前驱节点 p 是否为头节点(头节点着表示正在执行同步代码的线程)
- 当前线程是否获取锁成功(说明p已经释放了锁)
两个条件都满足才继续执行代码:将存储当前线程的节点置为头节点,GC掉p节点。
第二个 if 判断是否要阻塞(前驱节点的状态设置为signal后下一次自旋阻塞),执行 parkAndCheckInterrupt();,它内部调用了 LockSupport.park(),表示挂起当前线程。
PS:这是第三次cas获取锁。
三个方法大致了解后,回到最初的方法 acquire();,这里需要了解 逻辑与&& 的特性,若&&之前的条件不成立,那么 && 之后的条件就不会执行了(短路)。我们发现只有当获取锁失败的情况(未获取锁返回false)下才会执行后面的语句,即当cas获取失败,将线程以node的形式加入队列,并在队列中再次尝试获取锁(前驱节点是头节点的情况下才会尝试),如果获取失败,就此挂起。
PS:终于结束了:)
加锁总结:cas获取锁,失败则判断state是否为0(state=0表示没有线程持有锁,资源可访问。state>=1表示有线程持有锁,判断是否是当前线程持有),并通过cas尝试获取锁,失败则进入同步队列,若前驱节点是头节点,再次cas尝试获取锁,获取失败则将前驱节点的状态值置为signal,并挂起自己。
PS:非公平锁与公平锁的区别就是新来的线程在非公平锁的模式下会直接尝试获取锁,失败后再进入队列,进入队列后想再尝试获取锁就要排队等待条件满足!
解锁
首先,lock.unlock();,内部调用了 release() 方法
进入realse(),发现是AQS下的方法,内部调用了tryRelease(待会看)
如果满足条件的话,继续判断如果头结点不为空,状态不为0,则调用 unparkSuccessor() 方法(从队列中尾部往前遍历,唤醒一个离头节点最近的等待中的node( LockSupport.unpark() ),遇到CANCEL (即waitStatus=1) 的node直接跳过)。
private void unparkSuccessor(Node node) {
// cas操作修改头节点的状态(0表示无状态)
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// s表示头节点的下一个节点
Node s = node.next;
// 若s为空 或者 s.waitStatus > 0 表示节点的状态为取消状态
if (s == null || s.waitStatus > 0) {
s = null;
// //寻找后继中最近的且非取消的结点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// s不为空,则唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
进入tryRelease()方法,该方法会将state减去重入次数(可重入的情况下state>1),若state=0,说明释放锁,并将独占线程置为null。
PS:这里会判断当前线程是否是独占锁,避免其他线程无脑调用
解锁总结:将state置为0,唤醒下一个的节点,被唤醒的节点CAS获取锁。
PS:ReentrantLock是可以中断的,分为响应中断和忽略中断两种机制,但整个获取锁的过程是不响应中断的,只有最后获取之后才会告诉你是否有过中断,然后去进行中断!
图解
个人理解,如有不对,欢迎批评指正^^
参考文章:ReentrantLock和AQS源码解读系列一