AQS,全名AbstractQueuedSynchronizer。
- 抽象队列同步器
- 定义多线程访问共享资源的同步模板,解决了实现自定义同步器时涉及的大量细节问题,简化开发
- 两种同步状态:独占、共享
- 核心组件:State变量、CLH变体队列、获取 / 释放资源 方法重写
一、State变量
private volatile int state;
//返回同步状态
protected final int getState() {
return state;
}
//设置同步状态
protected final void setState(int newState) {
state = newState;
}
//使用CAS设置同步状态
protected final boolean compareAndSetState(int expect, int update) {
return STATE.compareAndSet(this, expect, update);
}
用关键字volatile修饰state
表示该共享资源的状态一更改就能被所有线程可见。
state为0时代表线程可以竞争锁,不为0时代表当前对象锁已经被占有。
ReentrantLock
的state
用来表示是否有锁资源ReentrantReadWriteLock
的state
高16
位代表读锁状态,低16
位代表写锁状态Semaphore
的state
用来表示可用信号的个数CountDownLatch
的state
用来表示计数器的值
二、CLH变体队列
AQS 维护一个等待的线程队列:
FIFO
(先进先出)队列,保证公平性- 双向链表形式,方便尾部节点插入
当一个线程竞争资源失败,就会将等待资源的线程封装成一个Node
节点,通过CAS
原子操作插入队列尾部,最终不同的Node
节点连接组成了一个CLH
队列,这些线程会被UNSAFE.park()
操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。
waitStatus
三、获取 / 释放资源 方法重写
AQS在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
1.独占资源(不响应线程中断)
- acquire(int arg):独占式获取资源模板
此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。
函数流程如下:
- tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
- addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
- release(int arg):独占式释放资源模板
函数流程如下:
- tryRelease()尝试直接释放资源,如果成功(state=0),要返回true,否则返回false。
-
unparkSuccessor(Node) 唤醒等待队列中下一个线程。
2.共享资源(不响应线程中断)
- acquireShared(int arg):共享式获取资源模板
函数流程如下:
- tryAcquireShared()尝试获取资源,成功则直接返回;
- 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。
跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)。
按照正常的思维,共享模式是可以多个线程同时执行的才对,所以,多个线程的情况下,如果老大释放完资源,但这部分资源满足不了老二,但能满足老三,那么老三就可以拿到资源。可事实是,从源码设计中可以看出,如果真的发生了这种情况,老三是拿不到资源的,因为等待队列是按顺序排列的,老二的资源需求量大,会把后面量小的老三以及老四、老五等都给卡住。从这一个角度来看,虽然AQS严格保证了顺序,但也降低了并发能力
- releaseShared(int arg):共享式释放资源模板
函数流程如下:
- tryReleaseShared:释放资源。
- doAcquireShared:唤醒后继结点。
四、应用实例:Mutex(互斥锁)
class Mutex implements Lock, java.io.Serializable {
// 自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判断是否锁定状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取资源,立即返回。成功则返回true,否则false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 这里限定只能为1个量
if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
return true;
}
return false;
}
// 尝试释放资源,立即返回。成功则为true,否则false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定为1个量
if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//释放资源,放弃占有状态
return true;
}
}
// 真正同步类的实现都依赖继承于AQS的自定义同步器!
private final Sync sync = new Sync();
//lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release。两者语文一样:释放资源。
public void unlock() {
sync.release(1);
}
//锁是否占有状态
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。
下面这些类中都包含Sync内部类。
五、参考
Java并发之AQS详解 - waterystone - 博客园 (cnblogs.com)
谈谈Java多线程离不开的AQS_java aqs-CSDN博客
Java并发编程面试题 | 小林coding (xiaolincoding.com)