锁的前世今生#
先来一个镇楼图,来自美团技术:
锁的升级过程可以参见《多线程之volatile和synchronized》

乐观锁与悲观锁#
- 悲观锁:对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。(不能并发修改,只能排队顺序)-------适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁:而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。 (可能并发修改,如果真并发修改了,再议)--------------适合读操作多(写少)的场景,不加锁的特点能够使其读操作的性能大幅提升。(没有加锁开销)
- 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
AQS#
AQS是啥#
AbstractQueuedSynchronizer抽象队列同步器简称AQS,它是实现同步器的基础组件,juc下面Lock的实现以及一些并发工具类就是通过AQS来实现的,这里我们通过AQS的类图先看一下大概,下面我们总结一下AQS的实现原理。
先说一句话描述:
AQS是一个通过双向链表实现的FIFO队列,并通过CAS的方式变更一个volatile的state字段和队列头尾来维护状态的同步器,它通过模板方法的方式暴露了一些留给子类实现的方法,可以实现多种并发工具的构建。AQS是juc很多类库的基础设施。
下面是详细描述:
(1)AQS是一个通过内置的FIFO双向队列来完成线程的排队工作(内部通过结点head和tail记录队首和队尾元素,元素的结点类型为Node类型,后面我们会看到Node的具体构造)。
1 | /*等待队列的队首结点(懒加载,这里体现为竞争失败的情况下,加入同步队列的线程执行到enq方法的时候会创 |
(2)其中Node中的thread用来存放进入AQS队列中的线程引用,Node结点内部的SHARED表示标记线程是因为获取共享资源失败被阻塞添加到队列中的;Node中的EXCLUSIVE表示线程因为获取独占资源失败被阻塞添加到队列中的。waitStatus表示当前线程的等待状态:
①CANCELLED=1:表示线程因为中断或者等待超时,需要从等待队列中取消等待;
②SIGNAL=-1:当前线程thread1占有锁,队列中的head(仅仅代表头结点,里面没有存放线程引用)的后继结点node1处于等待状态,如果已占有锁的线程thread1释放锁或被CANCEL之后就会通知这个结点node1去获取锁执行。
③CONDITION=-2:表示结点在等待队列中(这里指的是等待在某个lock的condition上,关于Condition的原理下面会写到),当持有锁的线程调用了Condition的signal()方法之后,结点会从该condition的等待队列转移到该lock的同步队列上,去竞争lock。(注意:这里的同步队列就是我们说的AQS维护的FIFO队列,等待队列则是每个condition关联的队列)
④PROPAGTE=-3:表示下一次共享状态获取将会传递给后继结点获取这个共享同步状态。
(3)AQS中维持了一个单一的volatile修饰的状态信息state(AQS通过Unsafe的相关方法,以原子性CAS的方式由线程去获取这个state)。AQS提供了getState()、setState()、compareAndSetState()函数修改值(实际上调用的是unsafe的compareAndSwapInt方法)。下面是AQS中的部分成员变量以及更新state的方法
1 | //这就是我们刚刚说到的head结点,懒加载的(竞争失败需要构建同步队列的时候,才会创建这个head),如果头节点存在,它的waitStatus不能为CANCELLED |
(4)AQS的设计师基于模板方法模式的。使用时候需要继承同步器并重写指定的方法,并且通常将子类推荐为定义同步组件的静态内部类,子类重写这些方法之后,AQS工作时使用的是提供的模板方法,在这些模板方法中调用子类重写的方法。其中子类可以重写的方法:
1 | //独占式的获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
(5)AQS的内部类ConditionObject是通过结合锁实现线程同步,ConditionObject可以直接访问AQS的变量(state、queue),ConditionObject是个条件变量 ,每个ConditionObject对应一个队列用来存放线程调用condition条件变量的await方法之后被阻塞的线程。
如果从来没有听说过AQS的同学要知道这个,可以先看下ReentrantLock,ReentrantLock实现了一个Lock接口,并持有一个私有抽象静态Sync对象,这个抽象Sync又有两个实现,分别是公平锁FairLock和非公平锁NonFiarLock。缺省是非公平锁。
- 公平锁:多个线程按照申请锁的顺序去获得锁,后申请锁的线程需要排队,等它之前的线程获得锁并释放后,它才能获得锁;
- 非公平锁:线程获得锁的顺序于申请锁的顺序无关,申请锁的线程可以直接尝试获得锁,谁抢到就是谁的;

具体的结构代码如下:
1 | public class ReentrantLock implements Lock, java.io.Serializable { |
而抽象类Sync继承的AbstractQueuedSynchronizer类(简称AQS,抽象队列同步器)是是一个可以用来实现线程同步的基础工具。也就是说我们常用的ReentrantLock底层是由AQS实现的。
也就是说:AQS使用模板方法的设计模式提供了同步功能的基础设施,可以用它来完成锁同步等功能。在Java的并发包中大量使用。
同步的抽象与各种实现:#
器AQS是公共逻辑,各种Lock的实现算是自定义的业务逻辑:
AQS和Sync、FairSync、NonfairSync都是公共的抽象逻辑,而Lock、ReadLock、ReentrantLock都算是业务逻辑。这些业务逻辑是有各个场景的特点,基于我们的公共抽象逻辑基础设施来实现的。

比如CountdownLatch里面也是有AQS和实现的。
此外ReentrantLock底层的Lock接口还保证了ReentrantLock的行为具有以下方法实现:

从ReentrantLock说起#
ReentrantLock作为JUC包提供的可重入锁,和Synchronized关键字的区别如下:
条件队列是指wait/notify或者await/signal

简单对比二者的使用
1 | // **************************Synchronized的使用方式************************** |
https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
ReentrantLock的源码#
1 | public class ReentrantLock implements Lock, java.io.Serializable { |
- 概要极简总结:
- ReentrantLock内部使用了一个AQS的模板实现Sync,这个Sync又针对公平锁和非公平锁有两种实现。
- 如非公平锁一上来就先CAS去修改AQS的锁状态state(unsafe用内存偏移量去修改,保证原子性),再去
acquire(1) - 公平的是上来就去
acquire(1) acquire(1)可以看下面AQS的部分
AQS的内部#
开门见山:
- AQS内部有一个双向链表实现一个FIFO的同步队列来维护当前获取锁失败的线程。
- 使用一个volatile的int类型的同步状态state和一系列方法实现同步。(state的各个值什么含义是给子类去实现的)
- AQS内部还有一个当前独占线程的标识,来标识谁在占用同步状态
AbstractQueuedSynchronizer使用了模板方法的设计模式,把大部分的流程都实现了,但关键步骤使用抽象方法、抛异常的方式,交给子类去强制实现个性化定制。如:
1 | // 独占地获取锁和释放锁(非共享读写锁) |


AQS提供的模板方法主要分为三类:
- 独占式地获取和释放锁;
- 共享式地获取和释放锁;
- 查询
AQS的同步队列中正在等待的线程情况;
双向链表实现的同步队列#
- AQS里面有一个volatile int类型的锁状态
state,多线程CAS竞争修改。 - AQS同步器内部有一个同步队列,每次线程获取锁失败就会增加一个Node到队尾,释放锁成功就会唤起队列最前面的Node中的线程,这个线程再去尝试。
- 队列中的头节点
head就是当前已经获取了锁,正在执行的线程对应的节点;而之后的这些节点,则对应着获取锁失败,正在排队的线程。 - AQS持有链表的
head和tail节点,每个Node节点里面除了pre和next还有当前的线程。 - 当一个线程获取锁失败,它会被封装成一个
Node,加入同步队列的尾部排队,同时线程会进入阻塞状态。 - 而当头节点对应的线程释放锁时,它会唤醒它的下一个节点。
- 被唤醒的节点对应的线程开始尝试获取锁,若获取成功,它就会将自己置为
head,然后将原来的head移出队列。

AQS加锁的源码#
acquire入口—核心方法#
ReentrantLock的lock()方法调用了AQS的下面方法:
1 | // 原文注释说:这是一个独占模式、忽略被打断。通过至少一次成功的tryAcquire就能拿到锁。 |
- 首先调用
tryAcquire尝试获取一次锁,若返回true,表示获取成功,则acquire方法将直接返回(没有构建同步队列);若返回false,则会继续向后执行acquireQueued方法;-------公平和非公平此处tryAcquire的实现有差异,非公平先去CAS竞争state了一次再去判断重入,公平的直接判断没有等待的(hasQueuedPredecessors)的线程才acquire。 tryAcquire返回false后,将执行acquireQueued,但是这个方法传入的参数调用了addWaiter方法;addWaiter方法的作用是将当前线封装成同步队列的节点,然后加入到同步队列的尾部进行排队,并返回此节点;(CAS去设置head、tail,失败则for死循环再来一轮。第一个head是一个空节点)addWaiter方法执行完成后,将它的返回值作为参数,调用acquireQueued方法。acquireQueued方法的作用是让当前线程在同步队列中阻塞,然后在被其他线程唤醒时去获取锁;-----这里会阻塞park- 若线程被唤醒并成功获取锁后,将从
acquireQueued方法中退出,同时返回一个boolean值表示当前线程是否被中断,若被中断,则会执行下面的selfInterrupt方法,响应中断;
tryAcquire子类实现的模板#
tryAcquire是一个模板方法,留给子类的公平锁、非公平锁按场景去实现。不同的场景根据这个arg去修改state字段。
1 | // 模板方法,留给子类去实现 |
公平锁实现的时候如果有等的更久的会不去抢,非公平锁上来就CAS抢state从0为1。成功就返回,没成功判断重入,重入返回true,否则false。
如果tryAcquire没竞争到锁,下面开始排队:
addWaiter-线程如何排队— 自旋添加到尾部,直到成功为止#
获取锁失败后到达addWaiter添加一个等待者:
将当前线程new 一个Node放到队列尾部,如果队列为空创建一个傀儡节点再添加尾部(傀儡节点就代表现在正在运行的那个线程)。如果加入失败就自旋(enq)直到添加成功,最后返回此节点。
(CAS去setTail一遍,失败的话后面enq循环一遍一遍CAS保证成功入队续上)
1 | private Node addWaiter(Node mode) { |
没初始化则初始化,然后入队尾,注意初始化的时候new了一个空的傀儡节点作为header,然后再来一轮for循环CAS续上的。(如果没抢到就再来for循环,直到CAS抢到再返回)
1 | /** |
所以ReentrantLock排队的过程是:#
- AQS使用一个双向链表来代表线程的先后顺序,head永远是空。AQS内部有status,0-free,>0-lock。Node内部有线程信息和waitStatus(CANCLE/SIGNAL/CONDITION/和共享锁的PROPAGATE)。
- 初始状态(也就是锁未被任何线程占用的时候)线程A申请锁此时,成功获取到锁,无排队线程(tryAcquire直接拿到true并返回)
- 线程B申请该锁,且上一个线程未释放:enq方法的for循环先创建一个空的Head(只有next指向)初始化队列,再来一个for循环添加当前Node(包含自身线程信息,和pre指向,模式独占或者共享)。
- 再来一个线程C申请该锁,且占有该锁的线程未释放:CAS来续尾成功就返回,失败的话去enq里面for的CAS续尾到成功为止。(成功后修改pre和自己的互相指向)
上面加入到队尾后,返回节点,传入下面的方法去获取锁。
acquireQueued—尾入队获取锁#
acquireQueued方法就是把获取锁失败的Node放入队列中,让这个线程不断进行“获锁”,直到它**“成功获锁”**或者“不再需要锁(如被中断)”
1 | /** |
方法的主流程如下:

shouldParkAfterFailedAcquire 判断获取锁失败后是否应该park阻断#
首先记着这个方法是在一个死循环中,获取锁失败就来执行一遍。为了方式for死循环大量占用CPU,可以想象绝大部分节点必然是返回true,然后park的。
1 | /** |
上面方法的流程图如下:


公平锁和非公平锁的体现#
“不管公平还是非公平模式下,ReentrantLock对于排队中的线程都能保证,排在前面的一定比排在后面的线程优先获得锁”但是,非公平模式不保证“队列中的第一个线程一定就比新来的(未加入到队列)的线程优先获锁”因为队列中的第一个线程尝试获得锁时,可能刚好来了一个线程也要获取锁,而这个刚来的线程都还未加入到等待队列,此时两个线程同时随机竞争,很有可能,队列中的第一个线程竞争失败(而该线程等待的时间其实比这个刚来的线程等待时间要久)。
因为非公平锁上来就CAS开抢:
1 | final void lock() { |
而公平锁hasQueuedPredecessors()返回false,没有排在自己前面的才能去抢:
1 | final void lock() { |
1 | static final class Node { |
Node 的类注释机翻
1 | <p> |
AQS锁释放的源码#
AbstractQueuedSynchronizer#release() 释放锁的方法,是底层释放锁的实现。
1 | // unlock的实现,就一行代码调用这个AQS的release |
ReentrantLock#tryRelease(1)释放锁,不用CAS,因为线程不一样释放不了。
1 | protected final boolean tryRelease(int releases) { |
解锁时唤醒后代:
1 | // 解锁时唤醒后代 |
后继节点唤醒之后,又回到上面的acquireQueued()方法,去进行下一轮死循环,判断pre是不是head,是不是已经解锁了,CAS抢占锁。
抢到后:head的next置为null,GC回去干掉head。把唤起的自己这个node变成新head,清理新head里面的thread信息和head里的pre信息。
然后当前唤起节点获取到锁的线程开始继续执行后面的业务方法。。。
ReentrantReadWriteLock读写锁(共享锁)#
独享锁:也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁:是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
下面是JDK的读写分离锁代码实现:
读写分离锁,在读比较多(耗时)的场合比常规的重入锁更加有效率。
- 读-读线程互不阻塞,多少各线程都可以并行一起读。
- 但是当
读-写或者写-写线程相互竞争的时候会阻塞获取锁才可以操作
ReentrantReadWriteLock内部:
-
持有一个int类型的status锁状态。
-
一个Sync同步器,同步器继承自AQS,并有公平非公平两种实现。
-
持有一个共享的ReadLock,一个独占的WriteLock。

可以看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。
在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。
类图如下:

读写锁的status设计#
AQS的时候我们也提到了state字段(int类型,32位),该字段用来描述有多少线程获持有锁。
- 在独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量。
- 但是在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。
- 于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。如下图所示:

代码中搞了一个EXCLUSIVE_MASK,是2^16-1。
通过和state求&可以拿到低16位的数据。
通过和state>>16位拿到高16位的读锁数据。
写锁的代码#
了解了概念之后我们再来看代码,先看写锁的加锁源码:
1 | protected final boolean tryAcquire(int acquires) { |
- 这段代码首先取到当前锁的个数c(这个数可能很大,是由高16位和低16位组合起来的),然后再通过c来获取写锁的个数w。:
- 因为写锁是低16位,所以取低16位的最大值与当前的c做与运算( int w = exclusiveCount©;
return c & EXCLUSIVE_MASK;这个EXCLUSIVE_MASK是2^16-1,是全1 ),高16位和0与运算后是0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。
- 因为写锁是低16位,所以取低16位的最大值与当前的c做与运算( int w = exclusiveCount©;
- 在取到写锁线程的数目后,首先判断是否已经有线程持有了锁。如果已经有线程持有了任何锁(c!=0),则查看当前写锁线程的数目,如果写线程数为0(即此时存在读锁)或者持有写锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。
- 如果本轮写后,写入锁的地位数量大于最大数(65535,2的16次方-1)就抛出一个Error。
- 如果当前写线程数为0(那么读线程也应该为0,因为上面已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;(非公平writerShouldBlock永远返回false,公平锁看队列有没有排队,有就true)如果通过CAS增加写线程数失败也返回失败。
- 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功!
后面的添加队列阻塞和唤醒与ReentrantLock相同。
上面可以看到tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。
因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。
接着是读锁的代码:#
读锁的lock()方法调用了这个
1 | public final void acquireShared(int arg) { |
下面是获取一次共享锁:
1 | protected final int tryAcquireShared(int unused) { |
可以看到在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。
获取一次读锁失败,进入下面进入队列阻塞,循环获取:
1 | /** |
此时,我们再回头看一下互斥锁ReentrantLock中公平锁和非公平锁的加锁源码:

我们发现在ReentrantLock虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。
根据源码所示,当某一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。
而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定ReentrantLock无论读操作还是写操作,添加的锁都是都是独享锁。
AQS的ConditionObject、await/signal#
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,它实现了java.util.concurrent.locks.Condition接口。(下文的Condition都指的是ConditionObject)
Condition的实现,主要包括:等待队列、等待和通知。
Condition使用了一个等待队列来记录wait的节点们(和之前的同步队列不是一个)
API示例#
lock.newCondition(); 给一个锁创建一个条件
1 | // 使用lock构造condition |
Condition的等待队列#
FIFO的队列,每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程。并不复杂。
如果一个线程调用了**Condition.await()**方法,那么该线程将会:
- 构造节点加入等待队列(没有CAS设置尾巴,因为前面必然获取到锁了)
- 释放当前线程的同步状态,唤醒后继节点,且当前线程进入WATING等待状态
- 当从await()方法返回时,当前线程一定获取了Condition相关联的锁。

节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。
1 | public class ConditionObject implements Condition, java.io.Serializable { |
await等待#
需要先lock.lock() 然后 condition.await(),await会释放掉锁,线程进入等待队列,状态变成Wating。
下次唤醒后再重新获取锁(不一定能获取到,获取不到再次for死循环获取进入同步队列,park)
1 |
|
调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
await具体执行流程如下:
- 调用addConditionWaiter将当前线程加入等待队列;
- 调用fullRelease释放当前线程节点的同步状态,唤醒后继节点;
- 线程进入等待状态;
- 线程被唤醒后,从while循环中退出,调用acquireQueued尝试获取同步状态;
- 同步状态获取成功后,线程从await方法返回。
如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。(获取锁的时候,同步队列的一个header必然释放了。然后等待队列尾巴新增加了一个Node,线程是当前线程。但从内容上讲几乎相当于移动。)

signal唤醒#
调用Condition的signal()方法将会唤醒再等待队列中的首节点,该节点也是到目前为止等待时间最长的节点。
1 | public final void signal() { |
step1:前置检查,判断当前线程是否是获取了锁的线程,如果不是抛出异常IllegalMonitorStateException,否则,执行step2;
step2:取得等待队列的头结点,头结点不为空执行doSignal,否则,signal结束。
1 | /** |
整个doSignal完成了这两个操作:调用transferForSignal将节点从等待队列移动到同步队列,并且,将该节点从等待队列删除。
怎么transfer的:
1 | /** |
- step1:将节点waitStatus设置为0,设置成功执行step2,否则(CANCLE)返回false;
- step2:调用enq方法将该节点加入同步队列;
- step3:使用LockSuppor.unpark()方法唤醒该节点的线程。
再次回顾我们AQS的enq方法:
1 | /** |
整个signal系列方法将线程从等待队列移动到同步队列可以总结为下图:
就是把等待队列(wait队列)的第一个节点transfer,通过enq方法丢到同步队列的tail上,更新tail。然后再unpark这个tail里面的线程。
被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。
成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。

Condition的signalAll()方法,将等待队列中的所有节点全部唤醒,相当于将等待队列中的每一个节点都执行一次signal()。
参考#
《Java并发编程的艺术》
https://www.cnblogs.com/tuyang1129/p/12670014.html
美团技术公众号的共享锁部分
https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html