作为java.util.concurrent
最核心的工具类,是该包各种多线程工具、容器实现线程安全的基石,重要性不言自明。想要成为一名优秀的Java程序员,对其源码的阅读、研究是一项基本要求。
下面我将和大家探讨一下我对AbstractQueuedSynchronizer
的理解、认识,如有理解、表述不对的地方,欢迎拍砖(轻点)~
开始奇妙之旅
AbstractQueuedSynchronizer,类名直译过来就是抽象的队列同步器
(后面简称为AQS
);
从类名上,我们可以得到三个关键词:Abstract
、Queued
、Synchronizer
。
我将从关键词Synchronizer
开始分析。
Synchronizer
Synchronizer
,同步器。为什么不直接称它为锁(Lock)呢?因为我觉得锁只是同步器的其中一个功能,比如锁要解决的问题是多个线程下对同一个资源的竞争
。而同步器额外的功能就是当多个线程对同一个资源发生竞争时,对多个线程进行协调
。
所以重新给Synchronizer
下一个定义:Synchronizer是协调多个线程对同一个资源竞争的同步器
,即 Synchronizer的功能
= 锁的功能
+ 竞争时的多线程协调功能
。
既然涉及到对资源的竞争,那么资源在哪呢?答案在AQS的代码中:
/**
* The synchronization state.
*/
private volatile int state;
这个用volatile
关键字修饰的属性(保证多线程下的可见性):state
便是多个线程竞争的资源。要想获得同步器这把锁,那么首先得竞争到state这个资源。
- 成功竞争到stats的线程,那么就获得了锁,继续执行业务上的代码;
- 若线程没有竞争到status,那么同步就将其挂起并协调它们进行下一次对state的竞争;
那么AQS是如何协调线程进行下一次对资源竞争的呢?答案在Queued
。
Queued
Queued
表明AQS内部是用队列的方式去协调多个线程在下次竞争的行为,即先进先出(FIFO),队列头结点将拥有优先竞争state的权利。
来看看代码,代码里用双向链表实现了一个CLH
队列,队列中的每个节点代表了一个线程及其状态(waitStatus)并记录了其前后节点。
对CLH队列感兴趣的同学可以看看其源码,这里就不展开了~
CLH队列解决了多个线程对资源竞争时线程的协调问题,那么协调行为(第一次竞争
和再次竞争
)又是在何时发生的呢?答案在Abstarct
。
Abstract
AQS把内部线程协调的数据结构(CHL队列)和行为(FIFO)定义好以后,那么剩下的问题就是何时进行竞争、何时进行再次竞争了,万事俱备、只欠东风;
AQS是一个抽象类,根据方法名可以很轻松的发现其protected方法的奥妙,这里重点看下tryAcquire
和tryRelease
方法,这是两个模板方法:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
从名字上可以很容易猜到这两个方法分别是尝试竞争资源和尝试释放资源的方法逻辑,而AQS将这两个逻辑实现下放到了其子类去实现、定义什么行为是属于资源竞争、什么行为是释放资源。这样大大提高了AQS的可扩展性,这也就是为什么AQS可以一个工具类承包了整个J.U.C包的同步相关的功能。
好了,说到这里,我们再来理一下思路:
- AQS首先在自身内部已经定义好了线程竞争时的数据结构(CLH队列)和协调下一次竞争行为(FIFO),剩下的问题便是何时触发竞争和下一次竞争了;
- AQS很巧妙的将何时触发竞争和下一次竞争的行为定义通过模板方法下放到了子类,由子类去决定和实现下。这样既能快速开发出功能丰富的同步器又能大大减少实现的复杂度。
来两个例子
上面说了那么多,不如来个例子实在。我们来看看ReentrantLock
可重入锁。
纵观ReentrantLock,发现它只有一个属性:Sync,所有的加锁、释放锁的操作都是基于Sync实例来做的。而Sync继承了AQS,并且有两个子类FairSync
公平锁和NonfairSync
非公平锁。
我们来看看它们是怎么定义资源竞争和资源释放行为的吧。
Sync
Sync类的方法不多,我们主要看下其中的tryRelease
方法:
protected final boolean tryRelease(int releases) {
// c为释放release个资源后,剩下的资源数
int c = getState() - releases;
//判断释放资源的线程是否是持有资源的线程
//若不是,则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//若c == 0,意味着该线程将不再持有资源
//那么释放资源并将owner线程置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
Sync定义了释放资源的逻辑,当资源被释放出来的时候,将会触发CLH队列中的线程对资源的竞争;
现在我们来看看FairSync公平锁和NonfairSync非公平锁是如何定义竞争资源的行为。
FairSync&NonfairSync
先来看看FairSync:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//资源是否空闲
if (c == 0) {
//CLH队列头是否有线程等待
if (!hasQueuedPredecessors() &&
//尝试用cas的方式竞争state资源
compareAndSetState(0, acquires)) {
//若成功则设置owner线程为自己
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;
}
再来看看NonfairSync:
//这部分代码在父类Sync中
//但是主要由NonfairSync,所以放在这里讨论
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;
}
从上面的代码可以看到公平锁和非公平锁的区别就是尝试获取锁的时候是否判断了CLH队列头是否有线程在等待,仅此而已。
通过分析,ReentrantLock
通过少量代码便实现了公平可重入锁和非公平可重入锁的功能。负责的线程状态维护、协调工作全部都放在了AQS内部,这样使得子类可以快速开发出功能丰富的锁
!
结束语
文章首先从类名对其应具有的功能和其基本工作原理、扩展方式进行了讨论,然后在此基础上对可重入锁的实现方式进行了分析。
好了,今天的AbstractQueuedSynchronizer奇妙之旅就到此结束了。
若有表述不对或不清楚的地方,欢迎大家一起探讨~ ^ ^