AbstractQueuedSynchronizer(AQS)是JUC的基石,是由伟大的Doug Lea设计完成。具体的设计过程可以看他写的论文,介绍的非常详细。AQS其实是一个抽象类,但内部已经帮我们实现了许多的核心方法。通过利用CAS操作与改进CLH队列来完成同步操作并且提供了一种细颗粒的锁机制,我们知道ReenTrantLock的底层就是通过实现AQS来完成同步操作。
在AQS中提供了下面五个方法的接口,供子类实现:
protected boolean tryAcquire(int arg)
protected boolean tryRelease(int arg)
protected int tryAcquireShared(int arg)
protected boolean tryReleaseShared(int arg)
protected boolean isHeldExclusively()
这里在实现过程中一般有两种模式,一种是共享模式、一种是独占模式。共享模式表示多个线程能够共享摸个锁,独占模式表示只有一个线程能够获取方法执行权,其他线程必须等待。下面直接看一下ReenTrantLock的实现原理。
ReenTrantLock默认是非公平锁,但是在使用ReenTrantLock的过程中,所有的操作都是委派至一个Sync类上,Sync类继承了AQS,如下图所示:
而NonfairSync表示已非公平锁形式实现AQS,FairSycn表示使用公平所形式实现AQS。
CLH队列
在AQS中,通过改进CLH队列结构来管理线程状态,以便完成tryLock、lockInterruptibly等操作。先看一下AQS中CLH队列的元素:
可以看到AQS将CLH队列设计成为一个阻塞队列,每一个节点表示需要竞争资源的线程,而每个节点有自身的状态,它只需要关注自身的状态与前驱节点状态就行,而且通过队列管理,对资源竞争的操作只出现在队列头与队列尾,这大大降低了系统的压力,提升了系统的性能。下面我们直接看源码。
之前介绍的ReentrantLock中有两种锁,一种是公平锁,一种是非公平锁。下面看一下公平锁的lock方法:
final void lock() {
acquire(1);
}
可以看到FairSync的lock方法直接调用了AQS的acquire方式来获取锁。我们看一下AQS中的acquire方法代码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里有3个方法需要我们注意,其实tryAcquire是AQS中的抽象方法,留给子类实现也就是ReenTrantLock来实现。若获取锁成功,则tryAcquire返回true,在这里若获取锁失败,则进入acquireQueued方法。我们先看一下addWaiter方法,看他会传递给acquireQueued方法什么东西。
private Node addWaiter(Node mode) {
//创建一个新的节点,mode为Node.EXCLUSIVE,表示的是获取锁的方式为独占方式
Node node = new Node(Thread.currentThread(), mode);
// 获取当前CLH队列中的队尾节点
Node pred = tail;
//若队尾节点部位空,则将需要插入的node的前置节点设为队尾节点pred
if (pred != null) {
node.prev = pred;
//将新节点node通过CAS操作插入到队尾,若修改失败则表示有其他节点竞争队尾位置,进入enq方法
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//进入enq方法
enq(node);
return node;
}
private Node enq(final Node node) {
//这是一个自旋操作
for (;;) {
//获取当前队尾
Node t = tail;
if (t == null) { // Must initialize
//若为空,则表示当前队列是一个空队列并没有线程持有锁,则新建一个node节点并使用CAS操作设置队列头节点。
if (compareAndSetHead(new Node()))
tail = head;
} else {
//若不为空,则继续自旋CAS操作,尝试将node设置为队尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
由上述方法可以看出,在AQS的CLH队列中,队列头表示拥有执行权的线程,后续线程需要获取锁,则需要加入到CLH队列尾部,若CLH队列为空,则表示该线程能直接获取锁。addWaiter返回一个node,代表着当前CLH的队尾。现在看一下acquireQueue方法的代码
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前节点的前一个节点,也就是我们刚才addWaiter返回的node节点的前置节点。
final Node p = node.predecessor();
//若该前置节点是头结点,且当前节点已经获取到锁,那么将node设置为头结点并返回。
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//若不是,则判断前一个节点的状态,看是需要挂起还是中断
if (shouldParkAfterFailedAcquire(p, node) &&
//若需要挂起,则使用LockSupport中的park方法把当前线程挂起。
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//若果有异常,则取消请求,将当前节点从队列中移除。
cancelAcquire(node);
}
}
现在我们在理一下lock方法的执行顺序:先执行acquire方法,acquire方执行时,会先调用自行实现的tryAcquire获取锁,若获取锁成功,则直接执行线程;若不成功,则为当前线程创建一个node,加到CLH队列尾部,可以看到需要竞争锁线程进入到CLH队列中后,就要开始控制访问了,通过acquireQueued方法进行自旋,直到线程获取锁或者中断退出,在获取锁的期间,线程会处于阻塞状态,直到被唤醒获取锁。在AQS中只有一个线程能够在同一时刻继续运行,其他的进入等待状态。每一个线程是一个独立的个体,他们自己观察,当条件满足的时候(前节点是队列头且其CAS获取锁1操作成功),那么该线程就能运行了。下面来看一下ReenTrantLock中对tryAcquire的两种实现方式
1.公平获取锁
protected final boolean tryAcquire(int acquires) {
//先获取当前线程并通过getState获取当前线程的状态
final Thread current = Thread.currentThread();
int c = getState();
//若为0则执行以下操作:
//1.通过hasQueuedPredecessors判断当前线程是否为CLH的头结点
//2.通过CAS操作设置当前节点的状态
//3.设置成功则让当先线程获取执行权,返回true
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//由于ReenTrantLock是可重入锁,所以这里获取当前拥有锁的线程,因为lock,unlock方法必须成对出现
else if (current == getExclusiveOwnerThread()) {
//进行计数
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}