这是一道面试题:简述AQS原理
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
看个AQS(AbstractQueuedSynchronizer)原理图:
AQS,它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:
getState()
setState()
compareAndSetState()
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
从源码的角度来分析(以ReentrantLock为例子)
所有的jdk实现的锁ReentrantLock、CountDownLatch、Semaphore通过内部匿名类继承了AbstractQueuedSynchronizer,然后重写AQS提供的部分方法。
在讲解reentrantlock之前先来看一下AQS的一些细节东西,因为reentrantlock类里面有三个静态内部类,这些静态内部类里面有些方法会用到AQS里面的方法,具体有哪些方法后面会提到,这里只是提下有个印象,三个静态内部类分别为:
//实现了AQS中的tryRelease(int releases),isHeldExclusively()方法,至于是抽象的是因为没有实现lock方法和 tryAcquire(int arg)的方法,因为这些方法是有其字类来实现的。
abstract static class Sync extends AbstractQueuedSynchronizer{}
static final class NonfairSync extends Sync{}
static final class FairSync extends Sync {}
下面部分图和文字参考了一些大神的。大神连接走你
1、原理及架构
AQS内部维护一个同步队列,所有的同步队列中的线程通过自旋方式不断获取同步状态。当一个线程无法获取同步状态时,通过创建一个Node节点并将此节点加入队列的尾部,此队列严格按照FIFO方式出入队列,只有当前线程是头结点并且成功获取到同步状态,此时的线程就可出队列;
state在AQS中被定义为一个volatile类型的的变量通过提供两种方法来让子类设置同步状态分别是compareAndSetState和setState,一种是通过原子CAS的方式,一种是普通方式,大致的架构如下:
首先看看Node(为AQS的一个静态内部类)节点的几个重要属性:
SHARED、EXCLUSIVE表示节点的模式,在创建节点是指定一种模式,一般情况比如对于文件系统的读操作,可以设置为共享模式,对于文件的写要设置为独占式保证线程安全的执行
waitStatus:节点的状态为三种:
1、CANCELLED表示线程处于超时或者中断状态,值为1
2、SIGNAL表示后继节点被唤醒,值为-1
3、CONDITION表示线程处于阻塞队列中,无法在同步队列中使用,值为-2,直到调用signal方法后将其转移到同步队列中
4、PROPAGATE表示下一个共享模式下获取同步状态会被持续传播下去,值为-3
一般情况AQS获取同步状态的方式分为共享模式和独占模式,下面先分析一下独占模式下AQS是如何获取一个同步状态的:其实它使用了一个模板设计模式。
tryAcquire(arg)方法有上面提到的字类方法去实现
// 独占模式下获取同步状态
public final void acquire(int arg) {
//尝试获取同步状态,如果获取失败,创建节点并且将节点加入到同步队列的尾部,并进行自旋状态,同时设置为阻塞状态
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
由于tryAcquire方法需要子类去实现,先分析一下这里的addWaiter:
//入参Mode为节点的模式,这里将获取不到状态的线程加入到同步队列中
private Node addWaiter(Node mode) {
//首先创建一个线程节点
Node node = new Node(Thread.currentThread(), mode);
//获取尾节点赋值当做新增节点的前驱
Node pred = tail;
if (pred != null) {
node.prev = pred;
//进行cas原子操作将新增节点加入到尾部,同时将前驱节点的后继节点设置为当前节点并返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果不存在同步队列或者尾部不存在
enq(node);
return node;
}
想一下:为什么要用CAS操作把当前线程node放入链表尾部节点?
答:因为,是多个线程去获取锁,如都没获取成功,则是不是都要把各自的node放入链表尾部,这样就有了并发了,因此加了CAS操作。
上述操作的图解如下图
addWaiter(node)方法中到数第二行有个enq方法,我们再来看一下:enq(node)方法
enq方法通过cas方式先去找尾节点,若为null,则初始化同步队列 这时,tail=head,否则按照正常的将新节点加入到尾部如上图所示。
//通过cas死循环
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
接着分析 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法:当新的线程未获取到同步状态后即上面代码中tryAcquire(arg)获取资源失败,则加入到同步队列中,此时通过此方法进行自旋,如下图所示:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前线程节点的前驱节点,如果是head节点则再次尝试获取同步状态,获取成功,将自身设置为头节点,前驱节点出队列
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//获取同步状态失败则设置该节点的状态,并且阻塞该节点
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
进行自旋时一旦发现前驱节点为head节点并且尝试获取同步状态,获取成功则设置当前节点为头节点,如下图所示:
shouldParkAfterFailedAcquire方法为当节点未获取同步状态时设置该节点的waitingStatus的状态,如果是被阻塞了直接返回true。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前驱节点已经被唤醒,则它是安全的被阻塞了,直接返回true
return true;
if (ws > 0) {
//如果当前节点的前驱节点状态为cancelled,则进入循环直到找到不为cancelled的节点,把此节点设置为当前节点的前驱节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//将前驱节点设置为signal
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
独占模式下同步状态的释放流程release方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
//获取头部节点不为Null,则唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
同理tryRelease(arg)方法由其字类方法去实现,这里以ReentrantLock为例子,它的静态内部类Sys类实现了这个方法。
这里重点看看unparkSuccessor方法,当唤醒后继节点后,由于进入同步队列的线程都处于不停地自旋状态,一旦符合前驱是head并且获取到同步节点则进行出队列
private void unparkSuccessor(Node node) {
//获取head节点的状态如果状态不是初始状态或cancelled,则设置为初始态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//如果后继节点是取消状态或者为Null,则循环从尾部节点开始往前找
Node s = node.next;
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;
}
//唤醒后继节点
if (s != null)
LockSupport.unpark(s.thread);
}
综上所述,AQS的一些原理讲解完了,下面结合例子ReentrantLock类默认非公平锁,看看如何集合AQS来使用的。
ReentrantLock中主要定义了三个内部类:Sync、NonfairSync、FairSync。上面已经提到,为了加深印象,这里再次提到其代码如下:
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
(1)抽象类Sync实现了AQS的部分方法如实现了AQS中的tryRelease(int releases),isHeldExclusively()方法,至于是抽象的是因为没有实现lock方法和tryAcquire(int arg)的方法,因为这些方法是有其字类来实现的。
(2)NonfairSync实现了Sync,主要用于非公平锁的获取。
(3)FairSync实现了Sync,主要用于公平锁的获取。
在这里我们先不急着看每个类具体的代码,等下面学习具体的功能点的时候再把所有方法串起来。
ReentrantLock中主要属性:private
final
Sync sync;
主要属性就一个sync,它在构造方法中初始化,决定使用公平锁还是非公平锁的方式获取锁。
ReentrantLock中构造函数:
// 默认构造方法
public ReentrantLock() {
sync = new NonfairSync();
}
// 自己可选择使用公平锁还是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
着重分析lock()方法,看他如何一步步和AQS结合的,默认是非公平锁,分析也是从非公平锁为例子。
接着跟进sys的lock函数,可以看到是个抽象函数,结合上面提到的是由其字类NonfairSync 来实现。
因此,我们跟进NonfairSync 的lock函数
final void lock() {
// 直接尝试CAS更新状态变量
if (compareAndSetState(0, 1))
// 如果更新成功,说明获取到锁,把当前线程设为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
从上述方法中看出,如果CAS更新state成功,则把当前线程设置为独占式线程,如果失败调用acquire(1)方法,把当前线程的信息分装成node节点加入到链表中(这里会和上面的AQS的一些代码结合起来,使用模板设计模式,我们看他是如何结合起来的)。
接着跟踪到acquire(1)方法,此方法在AQS里面被定义,tryAcquire(arg)在AQS里面是抽象方法,进去acquire方法后执行tryAcquire方法会进入NonfairSync(ReentranceLock的静态内部类)的tryAcquire方法里面,这也是使用模板设计模式来实现此效果的。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法前面已经分析过了,我们跟进tryAcquire方法
protected final boolean tryAcquire(int acquires) {
// 调用父类Sys的方法
return nonfairTryAcquire(acquires);
}
接着跟进其父类Sys(ReentranceLock的静态内部类)的nonfairTryAcquire方法。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果状态变量的值为0,再次尝试CAS更新状态变量的值
// 相对于公平锁模式少了!hasQueuedPredecessors()条件
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;
}
相对于公平锁,非公平锁加锁的过程主要有两点不同:
(1)一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;
(2)在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队呢;
总的来说,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。
分析完加锁了,接着分析unlock方法,看代码是不是很熟悉了,上述上面分析过了但是没有分析
tryRelease方法。
public void unlock() {
sync.release(1);
}
接着分析tryRelease()方法,这方法在哪里实现的就不说了,看到这里你肯定知道在哪里实现的。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程不是占有着锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果状态变量的值为0了,说明完全释放了锁
// 这也就是为什么重入锁调用了多少次lock()就要调用多少次unlock()的原因
// 如果不这样做,会导致锁不会完全释放,别的线程永远无法获取到锁
if (c == 0) {
free = true;
// 清空占有线程
setExclusiveOwnerThread(null);
}
// 设置状态变量的值
setState(c);
return free;
}
有一个细节要注意,上面方法设置状态变量的值即setState(c)为什么不用CAS操作?
答:因为是同一个线程,没有线程冲突。