AQS,本名:AbstractQueuedSynchronizer,是Java 5引入的一个并发工具类。
它提供了一个基于FIFO(先进先出)队列,可以用于构建锁或者其他相关同步器的基础框架。
它的名字翻译为抽象队列同步器,可以分为三个词:抽象、队列、同步器。
正好不知道怎么开始,那么现在我们就以名字的三个词作为切入点。但是为了逻辑讲得清晰,我调了一下顺序:同步器->抽象->队列。
1、同步器
这个类里面有一个最关键的属性:state,int类型。我觉得可以理解成两种意思,首先名称是state,直接翻译为状态(比如ReentrantLock里面的state用法),然后类型是int,可以理解为资源数量(比如Semaphore里面的state用法)。
它提供了getState和setState方法,还有一个线程安全的compareAndSetState方法。重点是后面这个,利用Unsafe进行CAS操作(如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值,相关资料可以参考上一篇博客:https://blog.csdn.net/qq_31142553/article/details/94407361)。
正是因为可以做到在并发场景下对state的修改是原子性的并且可以获取修改结果,所以基于这个特性可以将它做成一些同步器(类比Redis的setNx命令和Zookeeper的创建节点)。
2、抽象
抽象,说明它是可以被子类继承并且重写其中的一些方法。官方也是这么说的:Subclasses must define the protected methods that change this state, and which define what that state means in terms of this object being acquired or released。用我不太擅长的英语翻译过来就是:子类必须明确更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。因此这个类提供了以下方法
- tryAcquire(int):试图在独占模式下获取对象状态。
- tryRelease(int):试图设置状态来反映独占模式下的一个释放。
- tryAcquireShared(int):试图在共享模式下获取对象状态。
- tryReleaseShared(int):试图设置状态来反映共享模式下的一个释放。
- isHeldExclusively():如果对于当前(正调用的)线程,同步是以独占方式进行的,则返回 true。
它们的默认实现都是throw new UnsupportedOperationException();要求我们覆盖这些方法,定义哪种状态对于此对象意味着被获取或被释放。
这就用到了设计模式里面的模板方法模式:抽象父类定义了一个算法的所有步骤,而将其中一些实现交给子类,以满足不同的场景需要。
比如用作锁的话,state可以定义两个值:0表示未锁定状态,1表示锁定状态。
那么加锁就可以这样实现
// Acquire the lock if state is zero
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
return true;
}
return false;
}
释放锁可以这样实现
// Release the lock by setting state to zero
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) throw new IllegalMonitorStateException();
setState(0);
return true;
}
比如用作信号量(许可数量)的话,state可以表示剩余的数量。
那么获取所需资源就可以这样实现
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
释放资源就可以这样实现
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
3、队列
线程抢夺资源失败的时候,需要将其放进等待队列的末尾,然后将其挂起。等到资源释放的时候,拿出等待队列里面的第一个线程,让其继续去争抢资源。
因为这种首尾都会修改的使用特点,采用链表的实现方式远优于数组。定义了一个Note的内部类表示链表的节点,除了有用于维护链表连接关系的prev(前节点)和next(后节点)属性外,还有thread用于保存线程信息、waitStatus表示节点状态等。
下面是waitStatus的取值,注意默认值是没有意义的0。
AbstractQueuedSynchronizer中使用head和tail两个Note类型的属性存储链表的头结点和尾节点,从而可以修改或者遍历那个所谓的队列(或者说链表)。
如果把三者结合起来,就成了下面这个算法。
Acquire:
while (!tryAcquire(arg)) {
enqueue thread if it is not already queued;
possibly block current thread;
}
Release:
if (tryRelease(arg))
unblock the first queued thread;
我们看下ReentrantLock的非公平锁是怎么实现的
(1)lock方法
下面看下是怎么加入等待队列的(跟着上图中的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)部分)
(2)unlock方法
文章暂时讲到这里了,还有一些功能暂时没讲到,比如ConditionObject、超时、共享模式等,后续看情况补上吧。
AQS其实蛮复杂的,特别是队列里线程等待和唤醒的那部分,如果有讲错的地方,麻烦一定要在评论区里留言哦,万分感谢?。