[并发]AQS实现原理
AQS全称AbstractQueuedSynchronizer,是Java并发包里面一个基础类,我们熟悉的很多并发工具同时通过继承它来实现的,譬如ReentrantLock, Semaphore, CountDownLatch等等。类如其名,抽象的队列式同步器,AQS定义了一套多线程访问共享资源的框架,使用它能够简单且高效的构造出应用广泛的同步器。
基本原理
AQS名为队列同步器,内部就是通过维护了一个FIFO队列来完成获取资源线程的排队工作,即如果同时有多个线程想获取共享资源,等待的线程将会进入此队列。AQS无法确定共享的资源到底是什么,但是可以需要维护共享资源的同步状态。实际上,AQS内部用一个int变量state来代表同步状态, 用volatile修饰来保证线程可见性。private volatile int state;用户可以自己去实现如何表示同步状态。比如,如果一个线程成功获取了资源,那么state的值就加1;那么其它线程想获取资源时,看见这个同步状态state的值为1,便知道已经有一个线程获取了资源。相应的,如果一个线程释放了资源,state的值就减1,这样其它线程便知道了这个资源现在可以争抢使用了。
上面是如何实现同步状态的一个例子。state状态信息可以通过getState, setState和compareAndSetState方法进行重写和操作。
AQS支持两种资源的同步方式: 独占式(Exclusive) 和 共享式(Shared)。 独占式就是只能由一个线程去执行访问资源(如 ReentrantLock), 共享式则允许多个线程同时执行(如 Semaphore/CountDownLatch)。这样方便使用者实现不同类型的同步组件,可以自由发挥。
使用方式
AQS同步器的是基于模板方法模式的,使用时首先需要继承AQS类,并重写指定的方法。然后将AQS组合在自定义的同步组件的实现中,并调用模板方法。AQS定义一些可重写的方法:protected boolean tryAcquire(int arg): 独占式获取同步状态,成功返回true,否则返回false。protected boolean tryRelease(int arg): 独占式释放同步状态。protected int tryAcquireShared(int arg): 共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败。protected boolean tryReleaseShared(int arg): 共享式释放同步状态。 * protected boolean isHeldExclusively(): 是否在独占模式下被线程占用。
使用者可以重写这些方法来操作同步状态state,很明显需要tryAcquire和tryRelease搭配起来使用来实现独占式锁,tryAcquireShared和tryReleaseShared搭配起来使用来实现共享式锁。
重写的方法里面仅仅是简单的对于共享资源state的获取和释放操作就可以了。至于更复杂的逻辑,比如获取资源失败,线程阻塞/排队/唤醒之类的操作,AQS在顶层已经帮我们实现完成了。使用者只需要关注自己如何处理state同步状态这一部分事情就好,因此非常的方便。
接下来我们通过一段代码,自定义实现一个自己的锁,体会一下AQS的实际应用。
自定义锁
这里我们用AQS实现一个简单的锁,基本功能就是上锁和解锁,保证共享资源在上锁期间只有一个线程访问,也就是保证原子性。处理state同步状态的逻辑也很简单,跟前面举的例子一样。即: 一个线程成功获取了资源,那么state的值就加1;那么其它线程想获取资源时,看见这个同步状态state的值为1,便知道已经有一个线程获取了资源, 就进入等待。相应的,如果一个线程释放了资源,state的值就减1,当state值为0的时候,等待线程就能去争抢获得这把锁。
代码如下, 依然是通过注释来解析步骤。测试方式是用多个线程做共享变量count++操作来说明问题。因为count++非原子操作,在多线程下会产生不一致的情况。我们开启10个线程,每个线程给count加一千次1,在单线程环境下,count最终值应该是10000。这里先列出的是不给count++加锁的代码,在并发环境下,count最终的值达不到一万。
public class Main {
static int count = 0;
![[并发]AQS实现原理](https://m.ggpos.cn/uploads/202512/25/6c1bad4bf56fcbba.webp)
// 使用CyclicBarrier保证所有线程都运行完毕后查看结果
private static CyclicBarrier barrier = new CyclicBarrier(11);
// 初始化自定义锁
private static MyLock myLock = new MyLock();
public static void main(String args) throws Exception {
for(int i = 0; i < 10; i++) {
Thread t = new Thread(() -> {
for(int j = 0; j < 1000; j++) {
addOne();
}
try {
// 等候其它线程运行结束
barrier.await();
} catch (Exception e) {
e.printStackTrace();
});
t.start();
System.out.println("Count = " + count);
static void addOne() {
count++;
}运行上述不加锁的代码五次,结果分别如下,没有一次达到10000。
Count = 9443
Count = 9551
Count = 9724
Count = 9439
Count = 9313接下来我们用自定义的锁给count++加上锁,只需在addOne()方法中加入两行代码,其它代码保持不变。
myLock.lock();
myLock.unlock();
}依然运行五次,看看结果。
Count = 10000
Count = 10000结果都是10000,这说明我们自定义的锁成功了,保证了线程间独享变量,保证了count++的原子操作。