AQS简介
AQS全称AbstractQueuedSynchronizer,抽象队列同步器,是一个实现同步组件的基础框架。AQS使用一个int类型的成员变量state维护同步状态,通过内置的同步队列(CLH锁、FIFO)完成线程的排队工作,底层主要是通过CAS操作和volatile特性实现。
它主要包含两种模式:独占模式(例如ReentrantLock)和共享模式(例如CountDownLatch)。关键方法包括acquire和release,用于独占模式下的获取和释放操作,以及acquireShared和releaseShared,用于共享模式下的操作。
它简化了同步组件的实现方式,屏蔽了同步状态的管理、线程的排队等待与唤醒等底层操作。我们只需要通过继承实现AQS中的抽象方法,即可实现不同同步语义的同步组件。例如ReentrantLock这种独占式同步组件,核心逻辑仅重写了tryAcquire和tryRelease等少数方法,就实现了可重入独占式锁的功能。
AQS通过继承来扩展其功能,子类通过继承AQS实现抽象方法来管理同步状态,同步组件则通过聚合子类的方法来实现自己的同步特性,独占式或共享式。
使用AQS实现一个自定义排他锁
继承AQS:
创建一个新的类,继承AbstractQueuedSynchronizer。
实现核心方法:
实现tryAcquire和tryRelease方法(用于独占模式),或实现tryAcquireShared和tryReleaseShared方法(用于共享模式)。
内部类Sync:
通常创建一个内部静态类Sync,扩展AQS并实现上述方法。
对外方法:
在外部类中,提供获取和释放的方法,内部调用AQS的方法acquire、release、acquireShared和releaseShared。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class SimpleLock {
private final Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
使用AQS实现一个自定义共享锁
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class SimpleSemaphore {
private final Sync sync;
public SimpleSemaphore(int permits) {
sync = new Sync(permits);
}
private static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) {
setState(permits);
}
@Override
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining)) {
return remaining;
}
}
}
@Override
protected boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (compareAndSetState(current, next)) {
return true;
}
}
}
}
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void release() {
sync.releaseShared(1);
}
}
同步队列和条件队列的区别
同步队列(synchronization queue)和条件队列(condition queue)是AbstractQueuedSynchronizer(AQS)中的两个不同的队列,它们有不同的用途和工作机制。
同步队列
- 用途:管理所有等待获取锁或同步状态的线程。
- 触发条件:当线程尝试获取锁或同步状态失败时进入队列。
- 结构:双向链表。
- 作用:维护线程的顺序,确保公平竞争。
- 唤醒机制:当锁释放或同步状态改变时,唤醒队列中的下一个线程。
条件队列
- 用途:管理等待特定条件的线程(通过ConditionObject)。
- 触发条件:当线程调用await()方法时进入队列,并释放当前持有的锁。
- 结构:单向链表。
- 作用:等待特定条件的满足,如某个状态的变化。
- 唤醒机制:当条件满足时(通过signal()或signalAll()),线程从条件队列移动到同步队列,等待重新获取锁。
为什么await方法一定要加锁才能调用
我理解条件队列现在的实现是在await方法先完全释放锁,再进入休眠状态等待唤醒,被唤醒后重新尝试加锁。先完全释放锁,是为了防止当前线程持有锁时错误休眠,若当前线程持有锁进入休眠状态,即使当前线程被唤醒,也是尝试加锁而不是解锁,那么其他线程永远无法拿到锁。因此先完全解锁再休眠,被唤醒后重新尝试加锁,应该是出于防止锁饥饿与死锁的考虑。
AQS独占式加锁(以ReentrantLock加锁为例)
加锁流程
加锁源码分析
解锁流程
解锁源码分析
CLH锁(同步队列的原型)
CLH锁是对自旋锁的一种改良,自旋锁只适合竞争不激烈,加锁时间短的场景,原因是自旋锁有2个问题,第一,存在锁饥饿问题,可能会出现某个线程一直拿不到锁的情况;第二,锁状态中心化,激烈竞争时会导致CPU的高速缓存频繁失效,导致性能降低。
CLH队列锁有效地解决了以上2个问题,首先,CLH锁通过增加队列进行排队,确保所有线程都有机会拿到锁;第二,CLH锁的每个线程监视的都是上一个节点的锁状态,因此锁状态是去中心化的,不会导致CPU高速缓存频繁失效。
CLH锁实现源码
import java.util.concurrent.atomic.AtomicReference;
public class CLH {
private final ThreadLocal<Node> node = ThreadLocal.withInitial(Node::new);
private final AtomicReference<Node> tail = new AtomicReference<>(new Node());
private static class Node {
// ①锁定状态必须用volatile修饰以确保内存可见性
private volatile boolean locked;
}
public void lock() {
Node node = this.node.get();
node.locked = true;
Node pre = this.tail.getAndSet(node);
while (pre.locked) ;
}
public void unlock() {
Node node = this.node.get();
node.locked = false;
// ②重置当前线程对应的Node节点避免死锁
this.node.set(new Node());
}
}
原理分析
观察代码可以发现,CLH没有前驱或后继指针,因为CLH锁是一种隐式队列,入队只需要添加新的tail节点,出队只需要修改头部状态。注释①处必须用volatile修饰,以确保内存可见性与代码有序性。注释②处必须重置线程Node,否则当一个线程重复加锁时,就有可能死锁,死锁原因可以看这篇文章的解锁分析部分。
CLH锁的优点是性能优异、实现简单、加锁公平、扩展性强。但缺点是有自旋操作,长时间加锁会浪费CPU性能,再就是功能单一,不支持复杂的功能。
针对以上两个缺点,AQS都做了改进。针对第一个缺点,将自旋改为了线程阻塞等待;针对第二个缺点,AQS做了很多改造和扩展,例如增加了每个节点的状态waitStatus、显式维护了前驱和后继节点等等,基于这些扩展,AQS实现了独占锁、共享锁、线程排队、状态传播等复杂功能。
参考链接
从ReentrantLock的实现看AQS的原理及应用 - 美团技术团队
AQS 详解 | JavaGuide
AQS的前菜—详解CLH队列锁_clh锁-CSDN博客
Java AQS 核心数据结构-CLH 锁
JUC锁:核心类AQS源码详解 - 拿了桔子跑-范德依彪 - 博客园