在java的并发中,我们会接触到很多工具类,比如说ReentranLock,CountDownLatch,Semaphore,Condition。而这些工具类都是同门师兄弟来的,它们共同的师父就是我们这次文章讲的AQS,全名:AbstractQueuedSynchronizer。这个AQS几乎是java里面所有同步器和锁实现所依赖的框架。在面试中,也会有不少面试官会问到这个AQS。假如说你可以很明白的讲解出来AQS,那可是加分项。
在学习一个新框架或者新技术的时候,我们其实可以围绕3个点来去了解。
-
这个技术框架定义是什么?
-
有什么作用?
-
实现原理是怎么样的?
简称学习的三板斧!只要把这3个点弄明白了。那就算了解的七七八八了,可以添加到自己的知识库。后面再进行实践去熟悉和沉淀下来,融会贯通,能够学以致用了。
那我们先来了解一下AQS是什么。从源码开始。我从该类的注释里面直接截图给大家看看。
简陋的翻译一下:提供了一个依靠先进先出的等待队列来实现阻塞锁和同步器的框架。这个类设计目的就是设计出一个依靠简单的原子性状态标记实现,成为大多数各种各样的同步器的有用的基础框架。子类必须定义改变状态标记的方法。该方法包括了状态被获取和释放的规则。有了这2个方法,再加上这个类的其他方法就可以完成所有队列和阻塞器。子类可以包含其他状态值,但是只能使用getState,setState和compareAndSetState这几个方法 才能达到实现同步的目的。
顺便提一下,我们看源码的时候,可以通过阅读类的注释就可以知道这个类的设计思路,而很多网上对一些技术的科普,不少是直接从源码的英文注释翻译过来的。所以,只要我们把这些介绍的英文啃下来,弄明白。那就是最好了解该知识点的方式。
通过这里的定义,我们是可以知道AQS的设计目的了。而作用也说明白了。就是作为并发同步器和锁的实现的基础框架。那接下来,我们来了解实现原理。
AQS实现中有2个关键点,一个是锁的状态字段state,一个由链表实现的先进先出队列。state用来表明锁的状态,而队列用于等待线程的排队。在AQS中实现了setState,getState这些状态相关的方法,还有队列里类似新增节点,移除节点的方法,等等。
然后AQS是一个抽象类,给出了需要子类实现的方法,其他锁和相关同步器只需要重写父类的方法就可以实现自己的锁和同步器功能。AQS提供了独占模式和共享模式这两种模式。也就是我们经常说的独占锁和共享锁的来源。以下是AQS框架需要子类实现的方法:
tryAcquire //获取锁
tryRelease //释放锁
tryAcquireShared //获取共享锁
tryReleaseShared //释放共享锁
isHeldExclusively //是否拥有独占锁
为了更好的理解这几个方法,我们从ReentrantLock可重入锁的源码来看看它是怎么重写AQS的方法来实现锁的。
ReentrantLock中呢,也是提供了公平锁和非公平锁的两种模式。默认没有传参数就是非公平锁。
public ReentrantLock() {
//不传参数就是新建一个非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
//可以通过传参数来选择公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
其中Sync是ReentrantLock的一个内部类,它继承了AQS。
NonfairSync类和FairSync类都是是继承Sync的子类,tryAcquire()是AQS中获取锁的方法。FairSync重写了tryAcquire()方法,而NonfairSync是使用了父类Sync的tryAcquire()方法。
我们先来看看不公平锁NonFairSync的tryAcquire()方法:
protected final boolean tryAcquire(int acquires) {
//调用父类的方法
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//定义不可改变的变量存储当前线程
final Thread current = Thread.currentThread();
//获取AQS中的状态属性
int c = getState();
//判断如果是0的话,说明还没有其他线程获取锁,那可以尝试获取锁
if (c == 0) {
//使用AQS的compareAndSetState改变state标识,因为可能有多个
//线程同时获取锁,所以在判断后还是需要调用compareAndSetState
//来改变状态,compareAndSetState实现是unsafe类来实现的,我
//们可以默认为是线程安全的。
if (compareAndSetState(0, acquires)) {
//设置当前线程为得到锁的线程。
setExclusiveOwnerThread(current);
return true;
}
}
//可重复入的判断来了,假如已经获取锁的线程再次进来的时候就会
//发现自己就是得到该锁的线程。
else if (current == getExclusiveOwnerThread()) {
//将state加上acquires,也就是每重新进来一次,都加1.
int nextc = c + acquires;
//假如超出了int的正数的范围就报错
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
不公平锁这里的实现是根据AQS的state来实现的。在AQS的定义中,state是一个重要的属性。这个state是线程占有的标识,通过state这个标识来控制线程不被其他线程影响。在独占模式下,当其他线程获取到state是已经修改的,那说明已经有第一个线程在执行代码了,这时候就不能再执行了。
那我们再来看看公平锁的tryAcquire()实现。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//还是先判断状态属性值
if (c == 0) {
//唯一的不同点在这里,多一个判断,hasQueuedPredecessors,
//判断是否是队列里面有在排队的线程
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;
}
我们可以看到,公平锁就是需要从队列里面取下一个线程执行,而这个队列是先进先出的,所以保证公平。
我们再来看看释放锁的tryRelease()方法,NonfairSync类和FairSync类中,释放锁的方法都是使用父类Sync的方法。
protected final boolean tryRelease(int releases) {
//释放锁的时候需要传释放锁的次数,然后在拿state减去需要释放
//锁的次数,然后得出state的新的值
int c = getState() - releases;
//假如释放锁的线程不是占有该锁的线程,则抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//当锁被释放到没有占有时,则返回true,说明锁是没有了,然后把
//占有锁的线程值给置空
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
到这里,我们就知道ReentrantLock是怎么重写获取锁和释放锁的方法了。其中像setState,setExclusiveOwnerThread都是AQS中的方法。不需要ReentranLock这边去实现的。那ReentranLock是重写了AQS中的tryAcquire和tryRelease就可以实现自己的功能。
我们来看看另外一个使用tryAcquireShared和tryReleaseShared共享锁来实现自己功能的并发工具类:CountDownLatch。
CountDownLatch的作用是可以让指定数量的线程完成任务后,主线程再进行下一步操作。常用的方法就是
CountDownLatch.countDown()
CountDownLatch.await()
我们就从这2个方法来入手,首先是countDown()
public void countDown() {
//releaseShared是父类AQS实现的方法。
sync.releaseShared(1);
}
CountDownLatch这里也是使用了一个内部类Sync去继承AQS。
public final boolean releaseShared(int arg) {
//先释放共享锁,这里使用的是重写的tryReleaseShared
if (tryReleaseShared(arg)) {
//父类AQS实现方法,这个方法就是完成在释放锁后,把
//队列里面的线程释放出来获取锁,但是在CountDownLatch
//中其实没有使用到这个队列。
doReleaseShared();
return true;
}
return false;
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
//这里的注解也说明了。这里是做减一操作 ,当状态值state为0
//则返回true,说明现在CountDownLatch中的线程都已经执行完了
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
//如果减到0时,立即判断返回,不需要再循环一次。
return nextc == 0;
}
}
countDown方法简单来说就是对state进行减一。我们来看看await()方法。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//acquireSharedInterruptibly是AQS的方法,这里也是调用了
//tryAcquireShared方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
//父类方法
doAcquireSharedInterruptibly(arg);
}
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
//循环获取state,判断是否已经被释放。
//释放则return
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在await()方法中就是会调用到AQS中的doAcquireSharedInterruptibly方法,这个方法中关键的地方我已经注释了。其实就是当state变为0的时候会跳出循环。在CountDownLatch的效果就变成了线程都执行完了。所以简单来说CountDownLatch在一开始的时候就按照传入的参数,假如是3,把3赋值到state中,那每一个执行结束都把state减一。那等state等于0的时候就说明都执行完了。可以放行了。
通过这2个并发类,现在我们也对AQS的实现有了深刻印象了吧,AQS就是state和队列来实现。不过在这篇文章中,没有对AQS中的所有方法都进行分析一遍,这需要读者自己去阅读源码。不是作者懒,而是我觉得要想真正的把这些知识沉淀成自己的知识,是需要自己去实践,去阅读,才会深深的印在自己的脑海中。
为什么有时候你看了文章,以为自己懂了某个知识点,但是在面试的时候说不清楚呢?就是因为你的知识不还不扎实。而让知识扎实有挺多途径,比如说你把知识点都背下来,但是假如是自己去理解阅读的,再加以总结,那会比背下来的知识点更加好。也能更加好的领悟作者的思路。
授人以鱼不如授人以渔,小侠与你下篇文章见~
公众号:易小侠的Code