什么是AQS?
引言:
在Java并发编程中,锁和同步器是确保线程安全的关键工具。
其中,AQS(AbstractQueuedSynchronizer)是一个核心框架,为构建锁和其他同步组件提供了强 大的基础。
本文将深入探讨AQS的工作原理、与Synchronized的区别以及常见的实现类,并通过示例展示其用法。
- 全称是 (A bstract Q ueued S ynchronizer),即抽象队列同步器。它是构建锁或者其他同步组件的基础框架。
- 可以简单地将 AQS 理解为 Java 并发包( java.util.concurrent,简称JUC)提供的一种锁机制。
- 更准确地说,AQS 是一个用于构建锁和同步器的框架。它本身并不是一个具体的锁,而是提供了一套机制,使得开发者能够更容易地创建出符合自己需求的同步工具。
一、AQS 和 Synchronized 的区别
Synchronizer | AQS |
---|---|
关键字,c++语言实现 | java 语言实现 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
二、AQS 常见的实现类
- ReentrantLock 阻塞式锁,允许线程重复获取已经持有的锁,实现了可重入性。
- Semaphore 信号量,用于控制同时访问共享资源的线程数。
- CountDownLatch 倒计时锁,允许一个或多个线程等待其他线程完成操作。
三、AQS 的简单使用
AQS是一个抽象类,它本身并不能直接使用。我们通常通过继承AQS并实现必要的方法来创建自定义同步器。但是,为了简化说明,我们可以先看一下如何使用基于AQS实现的几个常见类,比如ReentrantLock。
3.1 ReentrantLock 示例
// 使用ReentrantLock 来确保 performTask 方法中的临界区代码在任何时候只能由一个线程访问。示例中启动了两个线程来执行任务,通过锁来保证线程安全。
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
lock.lock(); // 请求锁
try {
// 执行临界区代码
System.out.println(" 任务执行 " + Thread.currentThread().getName());
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
example.performTask();
}
};
// 启动两个线程执行任务
new Thread(task, "Thread-1").start();
new Thread(task, "Thread-2").start();
}
}
在上诉例子中,使用了 ReentrantLock 来保证 performTask 方法中的临界区代码在任何时候只能由一个线程访问。当一个线程获得锁并执行临界区代码时,其他尝试获得锁的线程将被阻塞,直到当前线程释放锁。
3.2 自定义同步器示例
基于AQS的自定义同步器示例。我们将实现一个简单的互斥锁(Mutex)
// 通过继承AQS并实现必要的方法来创建一个简单的互斥锁。
// 示例中展示了如何控制锁的获取和释放,并通过isLocked方法检查锁是否被当前线程占用。
public class Mutex {
// 自定义同步器,继承 AQS
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为 0 的时候获取锁
@Override
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 只能为 1
if (compareAndSetState(0, 1)) { // state为0才设置为1,不可重入
setExclusiveOwnerThread(Thread.currentThread()); // 设置为当前线程独占资源
return true;
}
return false;
}
// 释放锁,将状态设置为0
@Override
protected boolean tryRelease(int releases) {
assert releases == 1; // 只能为 1
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0); // state置0
return true;
}
// 提供一个方法,让我们的锁支持可重入
// 这里为了简单起见,我们不实现可重入功能,因此这个方法不会被使用到。
@Override
protected int tryAcquireShared(int acquires) {
throw new UnsupportedOperationException();
}
// 提供一个方法,让我们的锁支持可重入时的释放
// 同样地,为了简单起见,我们也不实现这个方法。
@Override
protected boolean tryReleaseShared(int releases) {
throw new UnsupportedOperationException();
}
}
// 同步器对象
private final Sync sync = new Sync();
// 获取锁
public void lock() {
sync.acquire(1);
}
// 尝试获取锁
public boolean tryLock() {
return sync.tryAcquire(1);
}
// 释放锁
public void unlock() {
sync.release(1);
}
// 锁是否被当前线程占用
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
在上诉例子中,我们创建了一个简单的互斥锁 Mutex ,它内部使用了一个自定义的同步器 Sync ,该同步器继承了AbstractQueuedSynchronizer。我们实现了tryAcquire和tryRelease方法来控制锁的获取和释放。
注意,为了简化示例,我们没有实现锁的可重入功能。在实际应用中,通常会更复杂一些,并需要考虑更多的边界条件和性能优化。
四、AQS-基本工作机制:
AQS内部维护了一个状态state和一个先进先出的双向队列。
state用于表示同步状态,队列中存储了等待获取锁的线程。
当线程尝试获取锁时,它会通过CAS操作尝试修改state。
如果修改成功,则当前线程获取了锁;否则,线程将进入队列等待。
当持有锁的线程释放锁时,它会唤醒队列中的头节点线程让其尝试获取锁。
这样就形成了一个简单的排队效果。
- 在 AQS 内部有个状态 state,state 是由 volatile 修饰的,来保证多线程的空间行,并且 state 有两个 int ( 1有锁 | 0无锁 ) 值。
- 假如现在有 线程 0想要获得一个这个锁,它获取修改 state 默认值 0 修改为 1,这样这个 线程 0 就有锁了。
- 当线程 线程 1来的时,state 已经是 1 了 , 线程 1修改失败,然后 线程 1 —> 到 FIFO 队列进行等待!
- 当线程 线程 2来的时,state 已经是 1 了 , 线程 2也修改失败,然后 线程 2 —> 到 FIFO 队列进行等待!
- FIFO 队列 是一个先进先出的双向队列,内部是一个双向链表,在FIFO还有两个属性分别为 head 和 tail
- 上述图中,**线程 0 ** 执行完了会将 state值修改为 0,同时它会唤醒 FIFO 队列队列中的 head元素让它去持有锁。
- 这就是一个简单的排队效果(AQS 基本的工作机制)
4.1 AQS-多个线程共同去抢这个资源是如何保证原子性的呢
- 目前 state 为 0
- 线程同同时来了 线程0和线程4,他们两个多要去修改 state,在这里面为了保证原子性用的是 cas 的设置
- 比如**线程 0 ** 它抢到了锁,它就会把 state修改为 1,**线程 4 ** 就要到队列中进行等待
五、总结:
- 什么是AQS?
- 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的像ReentrantLock、Semaphore都是基于AQS实现的
- AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
- 在AQS 内部还有一个属性 state,这个 state 就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
- 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性