本文作者:可乐可乐可,博主个人主页:可乐可乐可的个人主页
轻松理解AQS框架
本文需要以下知识铺垫:Java、临界区、信号量、锁
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是Java中重入锁ReentrantLock、读写锁、信号量的实现基石。
学会、了解AQS框架对了解Java锁有很大的帮助
说的比唱的好听,AQS源码下来2k+行,这是人干的活吗?
为了解决大家AQS不了解以及看了忘,忘了看的恶性循环,下面将带领大家从简到繁,一步步的学会AQS框架。
本文中涉及的代码以及做好了中文的注释,带伙可以访问我的github仓库拉下来看
github仓库地址:Jirath-Liu
AQS是啥
各位Java开发者必然会了解一个类,叫ReentrantLock。
在早期使用ReentrantLock效率是远远超过synchronized关键字的,现在差距一步步缩小了。
不知道有没有人点开过ReentrantLock的源码一探究竟?
ReentrantLock内部真正起作用的是Sync类,ReentrantLock所有跟锁有关的方法都调用了Sync的方法来实际实现。
而Sync的父类正是我们的主角——AbstractQueuedSynchronizer
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.lockInterruptibly();
}
public boolean tryLock() {
return sync.tryLock();
}
....
关于ReentrantLock这里就不在啰嗦了,如果有想了解的可以留言,给大伙安排上
我们现在关心的,是里面的这个Sync——AQS的实现类
Sync,或者说AQS的实现类,究竟做了什么,达到了加锁的目的?
加锁大家应该都知道是什么概念吧,信号想必比各位也应该了解(不清楚的先去搜搜
加锁的实质就是信号量,若有线程占用了某个资源,就在信号量进行标记,其他线程就了解这段临界区是被占用的。
我们AQS的原理其实就是信号量机制,Sync的机制如下图
这其中的过程都是由AQS来实现的,Sync编写了一些核心的判断来定制。
上图为Sync的源码,acquire是AQS的方法。
扯了这么半天,想必对AQS有个模糊的认识了:
一个实现了信号量,等待,抢夺锁的轻量级框架。
AbstractQueuedSynchronizer
提供一个框架来实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器(信号量,事件等)。
此类旨在为大多数依赖单个原子int值表示状态的同步器提供有用的基础。
子类必须定义更改此状态的受保护方法,并定义该状态对于获取或释放此对象而言意味着什么。
如何使用AQS来构建自己的锁?
我们先学会用AQS,再探知AQS的原理,总得先会跑,再想怎么跑步省力气吧
AQS框架的大佬给我们提供了四个需要我们实现的接口:
这几个方法都默认直接抛出异常:UnsupportedOperationException,需要子类继承来重写。
这四个方法都是干啥的?
AQS使用了模板方法的设计模式,这四个方法除了编写后直接使用外,更会被框架的其他方法调用。只要我们按照规矩老老实实的编写好这四个方法,就能得到一个出自自己之手的高效的轻量的锁。
这四个方法的功能就在下面了
// Main exported methods
/**
* 尝试以独占模式进行获取。
*
* 此方法应查询对象的状态是否允许以独占模式获取它,如果允许则获取它。
* 此方法始终由执行获取的线程调用。
* 如果此方法报告失败,则acquire方法可以将线程排队(如果尚未排队),
* 直到其他某个线程释放该信号为止。
*
* 这可以用来实现方法Lock.tryLock() 。
* 默认实现抛出UnsupportedOperationException 。
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
/**
* 尝试设置状态以反映排他模式下的发布。
* 始终由执行释放的线程调用此方法。
* 默认实现抛出UnsupportedOperationException 。
*/
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
/**
* 尝试以共享模式进行获取。
* 此方法应查询对象的状态是否允许以共享模式获取对象,如果允许则获取对象。
* 此方法始终由执行获取的线程调用。
* 如果此方法报告失败,则acquire方法可以将线程排队(如果尚未排队),直到其他某个线程释放该信号为止。
* 默认实现抛出UnsupportedOperationException 。
*/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 尝试设置状态以反映共享模式下的发布。
* 始终由执行释放的线程调用此方法。
* 默认实现抛出UnsupportedOperationException 。
*/
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 如果仅相对于当前(调用)线程保持同步,则返回true 。
* 每次调用非等待的AbstractQueuedSynchronizer.ConditionObject方法时,都会调用此方法。
* (等待方法改为调用release 。)
* 默认实现抛出UnsupportedOperationException 。
* 此方法仅在AbstractQueuedSynchronizer.ConditionObject方法内部内部调用,
* 因此如果不使用条件,则无需定义。
*
*/
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
当然,只拿了这四个方法,肯定是一脸懵逼的,AQS框架提供了很多的方法供子类使用,这些方法都是模板方法,final类型
大致有这些方法:
- 获取独占锁,以及各种姿势来获取(超时,响应中断,尝试等等),这些方法命名为acquire
- 获取共享锁,以及各种姿势来获取(超时,响应中断,尝试等等),这些方法命名为acquireShared
- 释放锁,包括释放独占锁和释放共享锁,释放锁是不处理线程争夺问题的
- 对等待(Wait)的操作
- 对AQS的感知,
- 是否有排队,目标线程是不是在排队
- 设置与获取当前的线程(在父类AbstractOwnableSynchronizer中实现),
- CAS设置信号量(compareAndSetState,本人认为这里称为信号量更合适),获取信号量,
总结下来AQS给用户提供了CAS获取锁,修改信号量,对AQS内部感知,锁操作的方法
这些方法一次堆上来就会眼花缭乱,我们可以从ReentrantLock中获取如何使用这些方法。
ReentrantLock中如何使用AQS
ReentrantLock中分为公平和非公平锁,这里的公平意思是在获取锁的时候,非公平的锁会直接尝试进行获取,而公平的锁会先看看自己会不会排在第一个,这意味着一个线程释放锁后再次获取锁,成功的几率会较其他线程高些。
我们先看公平锁的实现,来理解如何使用AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
final boolean tryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
//重入操作
} else if (