AQS
问题:
- AQS 是什么,有什么作用?
- 请讲讲 AQS 的原理
- AQS 底层数据结构具体是什么?Node 节点有哪些状态?
- AQS 对资源有哪些共享方式?
- AQS 底层实现,用了什么设计模式?
- AQS 在哪些同步器框架有应用?
1. AQS 是什么,有什么作用?
-
AQS (AbstractQueuedSynchronizer)是什么
AQS 是抽象队列同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它。
-
AQS 主要作用
为 Java 的并发同步组件提供统一的底层支持, ReentrantLock、 Semaphore、 CountDownLatch 等等就是基于 AQS 实现的
2. AQS 的原理
- AQS的实现依赖 FIFO 双向队列(CLH队列锁的变体)和 volatile 的 state 变量(共享资源状态)。
- 如果当前线程竞争失败, AQS 会把当前线程以及等待信息( Node ) 加入到队列中,同时阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点。
- volatile 能够保证多线程下的可见性。 state 的操作都是通过 CAS 来保证其并发修改的安全性。
2.1 state 同步状态的原子性管理:
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.2 CLH 锁
- CLH 锁是自旋锁,能保证无饥饿性,提供先来先服务的公平性。
- CLH 基于链表实现。线程只是在不断地自旋,不断地轮询前驱节点的状态,如果前驱结点释放了锁,那么线程结束自旋。
- **对于持锁时间很短的场景,**比之前把线程阻塞住具有较高的性能,并可以保持一定的公平性。
3.AQS 底层的同步队列的数据结构具体是什么?Node 节点有哪些状态?
3.1 Node 数据结构
Node 的源码部分:
static final class Node {
//共享模式
static final Node SHARED = new Node();
//独占模式
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
//节点状态
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//节点所对应的线程
volatile Thread thread;
//下一个等待者
Node nextWaiter;
//节点是否在共享模式下等待
final boolean isShared() {
return nextWaiter == SHARED;
}
//获取前驱节点,若前驱结点为空,抛出异常
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
Node 的节点状态:
- waitStatus : 当前 Node 实例的等待状态,可选值有 5 个。
- 每个节点都有其对应的状态,初始状态为 0。
- CANCELLED ,值为 1 ,表示当前的线程被取消
- SIGNAL ,值为 -1,说明该节点的后续被挂起了,当释放锁或者取消锁的时候,需要唤醒后继节点。
- CONDITION ,值为 -2 ,表示节点处于 Condition 队列中。
- PROPAGATE , 值为 -3 。用于共享锁,表示下一次尝试获取共享锁时,需要无条件传播下去
3.2 AQS 中两种同步队列的数据结构
- AbstractQueuedSynchronizer 类底层的数据结构是使用的双向链表,是队列的一种实现,Sync queue 即同步队列,是双向链表,包含 head 结点和 tail 结点, head 结点主要用作后续的调度。
- Condition queue 不是必须的,其实是一个单向链表,只有当使用 Condition 时,才会存在此单向链表。并且可能会有多个 Condition queue。
4. AQS 对资源有哪些共享方式?
- AQS 有两种资源共享方式
- Exclusive (独占) : 只有一个线程能执行,如 ReentrantLock
- Share (共享) : 多个线程可同时执行,如 Semaphore、CountDownLatch
- synchronized、ReentrantLock 是独占锁。ReadWriteLock 中的写锁是独占锁,读锁是共享锁。 Semaphore 、 CountDownLatch 都是共享锁。
5. AQS 底层实现,用了什么设计模式?
AQS 的设计基于模板方法模式,如果需要自定义同步器一般的方式是这样的:
- 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法
- 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法
模板方法:
**意图:**定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
**主要解决:**一些方法通用,却在每一个子类都重新写了这一方法。
**何时使用:**有一些通用的方法。
**如何解决:**将这些通用算法抽象出来。
**关键代码:**在抽象类实现,其他步骤在子类实现。
应用实例: 1、在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2、西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。 3、spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。
优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。
**缺点:**每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。
**注意事项:**为防止恶意操作,一般模板方法都加上 final 关键词。
public abstract class Game {
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
//模板
public final void play(){
//初始化游戏
initialize();
//开始游戏
startPlay();
//结束游戏
endPlay();
}
}
6. AQS 在哪些同步器框架有应用?
ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock都使用AQS (AbstractQueuedSynchronizer)实现独占锁或者共享锁。