AQS 详解

111 阅读4分钟

1、概述

AQS,即AbstractQueuedSynchronizer,抽象的队列式同步器。AQS定义了一套多线程访问共享资源的同步器框架,许多我们使用的同步器都是基于它来实现的,如常用的ReentrantLock、Semaphore、CountDownLatch、CyclicBarrie并发类都是通过实现AQS里面的模板方法来实现内部的组件。

基于AQS构建同步器:

  • ReentrantLock
  • Semaphore
  • CountDownLatch
  • ReentrantReadWriteLock
  • SynchronusQueue
  • FutureTask

2、基本框架

基本模型

AQS1113.png

获取同步状态:线程1获取状态由0修改为1,线程2获取状态为1就创建Node2结点放入队尾并且阻塞线程,后面依次内推

释放同步状态:线程1执行完成后,将state由1修改为0,并唤醒队列之后的Node2结点,并且删除Node1结点

AQS实现原理依赖内部State(同步状态)和CHL队列(FIFO双向队列),如果当前线程获取State同步状态失败,AQS就会将该线程以及状态等信息构造一个Node结点,并将这个Node结点添加到队尾,同时阻塞当前线程,当同步状态释放的时候,唤醒队列头结点

核心成员变量和方法

AQS的三个核心成员变量,都是用volatile关键字修饰的,保证了多线程下的可见性

/**
 * Head of the wait queue, lazily initialized.  Except for
 * initialization, it is modified only via method setHead.  Note:
 * If head exists, its waitStatus is guaranteed not to be
 * CANCELLED.
 */
private transient volatile Node head;
​
/**
 * Tail of the wait queue, lazily initialized.  Modified only via
 * method enq to add new wait node.
 */
private transient volatile Node tail;
​
/**
 * The synchronization state.
 */
private volatile int state;

head:是CHL队列的头结点,延迟初始化。除了初始化,只可以通过setHead() 方法进行修改

tail:是CHL队列的尾结点,延迟初始化,,只可以通过enq() 方法新增等待的节点

State:同步状态可以通过

  • getState():获取同步状态。
  • setState(int newState):设置同步状态。
  • compareAndSetState(int expect, int update):通过CAS方式修改同步状态。

资源共享方式

  • 独占式(Exclusive):只有单个线程能够成功呢获取资源并执行,如ReentrantLock。
  • 共享式(Shared):多个线程可成功获取资源并执行,如Semaphore、CountDownLatch等。

AQS将大部分的同步逻辑均已经实现好了,继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码就可以了。 独占

  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
public class Demo111 {
        Lock lock=new ReentrantLock();
        AbstractQueuedSynchronizer aqs=new AbstractQueuedSynchronizer() {
                @Override
                protected boolean tryAcquire(int arg) {
                        //独占方式。尝试获取资源,成功则返回true,失败则返回false。
                        return super.tryAcquire(arg);
                }
​
                @Override
                protected boolean tryRelease(int arg) {
                        //独占方式。尝试释放资源,成功则返回true,失败则返回false。
                        return super.tryRelease(arg);
                }
​
                @Override
                protected int tryAcquireShared(int arg) {
                        //共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
                        return super.tryAcquireShared(arg);
                }
​
                @Override
                protected boolean tryReleaseShared(int arg) {
                        //共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
                        return super.tryReleaseShared(arg);
                }
​
                @Override
                protected boolean isHeldExclusively() {
                        //该线程是否正在独占资源。只有用到condition才需要去实现它。
                        return super.isHeldExclusively();
                }
​
                @Override
                public String toString() {
                        return super.toString();
                }
                
        };
}

一般自定义同步器不是独占就是共享方式,只需要实现tryAcquire-tryRelease或者 tryAcquireShared-tryReleaseShared 中的一组就可以了,AQS也支持子类同时实现独占和共享两种模式,如ReentrantReadWriteLock

基本原理

AQS是一个抽象类,可以用来构造锁和同步类,如ReentrantLock,Semaphore,CountDownLatch,CyclicBarrier。

AQS的原理是,AQS内部有三个核心组件,一个是state代表加锁状态初始值为0,一个是获取到锁的线程,还有一个阻塞队列。 当有线程想获取锁时,会以CAS的形式将state变为1,CAS成功后便将加锁线程设为自己。当其他线程来竞争锁时会判断state是不是0,不是0再判断加锁线程是不是自己,不是的话就把自己放入阻塞队列。这个阻塞队列是用双向链表实现的

可重入锁的原理就是每次加锁时判断一下加锁线程是不是自己,是的话state+1,释放锁的时候就将state-1。当state减到0的时候就去唤醒阻塞队列的第一个线程。

为什么AQS使用的双向链表?

因为有一些线程可能发生中断,而发生中断的时候就需要再同步阻塞队列中删除掉,这个时候用双向链表就方便删除掉中间的结点# AQS 详解