AQS源码分析
AQS是Concurrent包下的核心类,很多的类包括ReentrantLock, CountDownLatch, CyclicBarrier, ReadWriteLock等包都是通过AQS这个类实现的。
这篇文章将会和大家一起看一下AQS的源码分析,从而了解AQS的内部运行情况。
AQS的核心内容包括三大块(先了解一下):
state状态:0表示锁处于释放状态,1-n表示有线程取得锁,可以为n表示可重入锁;
queue双向链表:没有拿到锁的线程进入queue中等待;
CAS保证原子操作:因为可能多个线程同时去争抢锁,也就是修改state的值,还有多个线程加入queue队列,所以,通过CAS保证原子性(它的高效性体现在如果不使用CAS,而是对整个queue加synchronized锁,效率会很低)。
先看一下总体流程图,看不懂没关系,下面一步步分析;
流程简介:
当一个线程通过reentrantlock.lock()获取锁的时候,
会先尝试CAS修改state的状态从而获取锁,成功,直接修改state的状态;
如果失败,再次尝试修改状态,此次允许可重入;
如果失败,进入等待队列,然后自旋等待状态被释放;
这个过程中,只有队列的第二个节点会进行tryAcquire尝试再次获取,
其他节点,尝试阻塞;通过lockSupport.lock();
AQS的实现的锁的种类:
-
公平和非公平:一个线程通过lock()进来的时候是否直接.acquire()抢锁还是先去队列判断有没有等待的线程;
-
可重入:state只有0/1 还是可重入
-
共享和独占:共享锁相比与独占锁,可以多个线程同时拿到锁,原理:propogate,每一个线程拿到锁后,循环让下一个线程来拿锁;会先判断是否被独占。 没有详细看,只是看了一下流程?
-
ReentrantReadWriteLock, 基于共享锁的实现;
-
Semaphore, 可以控制并发量。基于共享锁的实现;每次.await()将Semaphore-1; 当semaphor信号量减到0的时候,所有的线程propogate获取锁;
-
CountDownLatch 其他线程.countdown(); latch–;主线程.await(), 当latch为0的时候,其他线程.await()释放
也是共享模式,多个线程可以CAS操作state–; 当state为0的时候,所有.await()线程开启;
-
CyclicBarrier .await(). state–。 state为0后,所有的线程开启。
下面,我们以ReentrantLock加锁解锁的过程为例来分析这个过程。
1.这是一个demo
我们获得lock锁,然后对其lock加锁和unlock解锁。接下来看一下lock的源码;
2.ReentrantLock调用Sync类中的lock方法。
Sync什么类呢?它是ReenntrantLock的内部类,
通过看它的继承树可以直到,它是AQS的子类。 NonSync–> Sync --> AQS
3. 再来看sycn中的lock方法
首先这里调用了CAS去抢锁。如果成功了,就设置为exclusive独占锁。如果失败了,调用acquire(1)方法。
- 再来看一下acquire方法。
acquire方法,分成了3步,
第一步:再次尝试获得锁,获取成功,就获得锁,而且这里会进行重入锁的判断,看4.1
第二步:addWaiter,通过CAS操作将当前Thread构建node加到队列尾中;看4.2
第三步:acquireQueued,再队列中不断尝试获取锁,或者挂断;看4.3
4.三步
4.1 tryAcquire调用的是nonfairTryAcquire(), 默认的是非公平锁,所以这里,直接放这个方法。在这里会进行可重入锁的判断。
4.2 如果没有获得锁,加入队列。这里的2.1 CAS加入队列会尝试加锁,如果失败后,if之前的node.pre = pred还是执行,可能会出现多个node的前node指向了同一个node。 因此,失败后,在enq方法中再次尝试CAS操作。
4.3 加锁队列后,acquireQueue会循环尝试获得锁。
这里分成两块,
如果当前node的前置节点是个head,也就是正在拿到锁的运行线程,那么它可能即将获得锁,因此,tryAcquire尝试获得锁,如果还是失败,在进入3.3 挂起的条件,如果满足,就挂起,等待前置节点完成后,唤醒它。
第二块,非第二个node,也就是当前节点的前置节点不是head,那么它可能等待的时间较长,会直接进入3.3尝试挂起.等待前置节点完成后,唤醒它。
接下来,看一下unlock。释放锁的代码
5. 释放锁
6. tryRelease
7. unPark
8. 公平锁和非公平锁
public ReentrantLock() {
// 默认非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
static final class NonfairSync extends Sync {
final void lock() {
// 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 这里没有对阻塞队列进行判断
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
总结:公平锁和非公平锁只有两处不同:
- 非公平锁在调用 lock 后,会调用 CAS 进行一次抢锁
- 在 tryAcquire 方法中,如果(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
总结
在并发环境下,加锁和解锁需要以下三个部件的协调:
锁状态。 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
阻塞队列。
争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
阻塞队列。