【Java 并发】【九】【AQS】【一】什么是AQS?为什么说它是JUC基础框架?
1 前言
这节我们来开始看 AQS,这个东西可以说是搞Java的都知道的,本节会介绍一下AQS以及它提供的基本机制,后面再对AQS提供的每一个机制一个个深入的剖析。
2 什么是AQS?(同步器基础框架)
AQS叫做抽象队列同步器(AbstractQueuedSynchronizer),它是一个实现了同步器功能的基础框架,其它的同步工具类基于它去实现比如我们用到的ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch等等。
为什么又说它是同步器的基础框架,比如我们项目现在用的spring、mybatis框架等,就是对很多底层细节、底层很多功能进行了封装,极大程度的简化了我们的开发。
- 就比如mybatis我理解为是一个sql层次或者dao层的一个框架,帮我们做了java实体类、和数据库字段映射、帮我们封装了底层JDBC的功能等。
- 比如springmvc框架,我理解作为了一个servlet框架,对servlet进行了封装,请求url到servlet之后怎么映射到你的方法上面,请求的参数怎么映射到你的方法参数上面等等细节,极大的简化了我们的开发。
这些都是我们平时开发时候经常使用的框架,这些框架对很多基础的功能和底层的细节进行了封装,在使用层次极大的简化了我们的开发。那么AQS它也是一个框架,只不过它是一个并发工具类的底层框架。
在详情了解AQS之前,我们先来看下如果你自己封装一个通用的同步器,应该包含哪些功能呢:
(1)首先线程1去同步器获取资源,即获取锁,获取锁成功直接执行业务方法
(2)线程2也获取锁,但是获取锁失败,就会进入等待队列,阻塞等待
(3)当线程1释放锁的时候,需要去唤醒还在等待锁的线程2,然后线程2苏醒之后继续去尝试获取锁。
我们再来画个图理解一下:
每个同步器都有资源的概念也就是锁、获取锁失败之后,需要阻塞等待,释放锁之后唤醒等待的线程,也就是AQS封装的通用功能。
为什么不也连获取锁、释放锁也封装了?AQS作为一个框架,之封装了通用的功能;那些获取锁、释放锁的逻辑每个同步器都不一样,这些是非通用的,需要具体的同步器去实现了。ReentrantLock、CountDownLatch、Semaphore等使用了AQS框架的基本机制,然后自己实现了获取锁、释放锁的具体逻辑,这样就形成了不同的同步器了。我们再看看哪些并发工具基于AQS实现的:
那么我们接下来就来看看AQS内部都有哪些东西。
3 AQS内部有哪些东西
我们先从整体上看一下:
(1)首先对资源进行了定义,使用一个volatile int state表示资源
(2)规定了获取独占资源的入口为acquire()方法、释放独占资源的入口release()
(3)规定了获取共享资源的入口acquireShared()、释放共享资源入口 releaseShared()方法
(4)声明了实际获取资源、释放资源的具体实现方法,AQS这里只是声明,由子类去重写,实现获取和释放的逻辑。
实际获取独占资源、实际释放独占资源、实际获取共享资源、实际释放共享资源的入口方法,没有具体的实现,让子类实现这些方法从而形成不同的同步工具,这些抽象方法包括:
(4-1)实际获取独占资源的方法,具体实现逻辑封装在子类的tryAcquire()方法内部
(4-2)实际释放独占资源的方法,具体实现逻辑封装在子类的tryRelease()方法内部
(4-3)实际获取共享资源的方法,具体实现逻辑封装在子类的tryAcquireShared()方法内部
(4-4)实际释放共享资源的方法,具体实现逻辑封装在子类的tryReleaseShared()方法内部
(5)封装了一个Node的数据结构,用来存储线程信息。通过多个Node串成一个双向链表的等待队列;链表存储了获取锁失败而进入等待队列的线程
(6)封装了一套非常核心的,线程获取资源失败而如何进入等待队列;以及释放资源之后怎么唤醒等待队列中的线程再次竞争资源的这么一套机制。
这套机制非常核心,基于AQS之上的同步工具类底层都是使用这套机制来实现的。上面的AQS内部包含的数据结构、定义的入口和机制,再用一个图来整理一下:
那么接下来我们来一个个了解:
3.1 对资源进行定义 (state 表示资源)
作为一个同步工具,肯定是存在一个多个线程可以共同访问的资源,通过这个资源的状态可以控制各个线程并发时候的行为(也就是通过是否获取锁控制不同线程的行为)。
AQS底层肯定也会有资源的概念,AQS使用的是一个volatile int state的变量表示资源。
AQS只是说state表示的是资源,至于子类怎么使用state,state表示什么意思是子类决定的,AQS自己不管的哈。
(1)比如ReentrantLock中,表示state就表示互斥锁的状态,state = 0表示没人加锁,state > 0 表示有人加锁了;
(2)比如Semaphore就使用state表示信号量的个数。state = 10就表示有10个信号量,state > 0的时候信号量还有剩余,别的线程可以去获取,state = 0的时候表示没信号量了,这个时候再去获取就要等待了。
比如下图,AQS使用state表示资源,多线程并发竞争资源:
AQS对资源进行了声明,也就是告诉子类,我内部的state变量表示的是资源。至于子类你怎么使用我管不着。
3.2 获取资源和释放资源的入口进行了规定
既然AQS定义了什么是资源,所以AQS同时肯定会定义一套获取资源、释放资源的入口或者规定,如果要基于AQS实现同步工具类啊,就都要遵守AQS的这套规定。
3.2.1 对获取和释放独占锁的入口进行规定
(1)accquire(int arg):获取独占锁入口,获取独占锁需要调用这个方法
(2)acquireInterruptibly(int arg):跟上面的acquire()一样,也是获取独占锁的入口,不过是允许中断的,获取过程中线程被中断了就会抛出异常。
(3)release(int arg):释放独占锁的入口,当释放锁的时候调用这个方法
获取和是释放独占锁的流程如下图:
3.2.2 对获取和释放共享锁的入口进行规定
(1)acquireShare(int arg):获取共享锁入口,获取的时候需要调用这个方法
(2)acquireShareInterruptibly(int arg) :跟上面的acquireShare(int arg)一样,也是获取共享锁的入口,不过是允许中断的,获取过程中线程被中断就会抛出异常。
(3)releaseShare(int arg):释放共享锁的入口,释放共享锁的时候调用这个方法
AQS规定了获取锁和释放锁的入口,子类全部都要遵守这个规则。
比如说要获取互斥锁要调用的是acquire(int arg)这个方法,释放互斥锁调用的是release(int arg)这个方法。
获取共享锁调用的是acquireShared(int arg)这个方法,释放共享锁调用的是releaseShared(int arg)这个方法。
对实际获取和释放的实现逻辑进行了定义,具体逻辑由子类实现:
(1)tryAcquire(int arg):获取独占锁的具体逻辑,AQS只是对方法进行了定义,没有实现,具体实现逻辑交给子类去做
(2)tryRelease(int arg):释放独占锁的具体逻辑,AQS只是对方法进行了定义,没有实现,具体实现逻辑交给子类去做
(3)tryAcquireShared(int arg):获取共享锁的具体逻辑,AQS只是对方法进行了定义,没有实现,具体实现逻辑交给子类去做
(4)tryReleaseShared(int arg):释放共享锁的具体逻辑,AQS只是对方法进行了定义,没有实现,具体实现逻辑交给子类去做。
这个是什么意思呢?就是说子类如果要实现一个独占锁或者共享锁、或者是读写锁的功能就有下面几种情况:
(1)如果子类要基于AQS之上实现独占锁的功能,就要继承AQS,然后重写AQS的tryAcquire()、tryRelease()这两个方法。
(2)如果子类要基于AQS之上实现共享锁的功能,就要继承AQS,然后重写AQS的tryAcquireShared()、tryReleaseShared() 这两个方法。
(3)如果子类要基于AQS之上实现读写锁的功能,就是同时具备读和写两种锁,那么上面的tryAcquire()、tryRelease()、tryAcquireShared()、tryReleaseShared() 这四个方法都要实现。
上面的关系说的这些内容,再画图来理解一下:
上面的acquire、acquireShared方法等,作为一个入口提供外层调用;实际上内部会调用对应的tryAcquire、tryAcquireShared方法,这些try开头的方法实际上才是去真正去获取锁的。
AQS为啥要这么设计呢?直接设计acquire、acquireShared等方法作为抽象方法,让子类实现这些方法不就行了吗?为啥还搞个tryAcquire、tryAcquireShared方法让子类去实现?
这就是AQS对模板方法的设计模式应用了,比如结合一个acquire方法内部的源码看一下:
public final void acquire(int arg) { if (!tryAcquire(arg) && // 1. 调用子类的tryAcquire方法实际获取独占锁 // 2.调用tryAcquire失败会执行到addWait方法 // 3.执行acquireQueued方法 acquireQueued( addWaiter(Node.EXCLUSIVE), arg) ) selfInterrupt(); }
看上面的源码acquire方法作为一个入口方法,它其实是一个模板方法,有下面的模板流程。
(2)流程2:如果争抢资源失败则执行addWait方法
(3)流程3:如果争抢资源失败则执行acquireQueue方法
public final void acquireShared(int arg) { // 1.调用子类的tryAcquireShared方法去获取共享锁 if (tryAcquireShared(arg) < 0) // 2. 获取失败则走到模板方法的第二个流程 doAcquireShared(arg); }
相当于acquire、acquireShared方法作为入口,定义了一套模板机制;同时tryAcquire、acquireShared作为模板流程中的一环,由子类实现。这样能保证走同一套流程机制,同时子类又可以实现不同的逻辑,保证了多样性和拓展性。
3.3 Node节点和等待队列
定义好了获取资源的入口、规定了由子类去实现具体的获取逻辑之后定义好了获取资源的入口、规定了由子类去实现具体的获取逻辑之后但是如果一个线程获取资源失败会怎么样?线程是立即返回获取锁失败呢,还是说我要进入等待,等待别的线程释放锁之后我再去获取?所以AQS底层定义了两个数据结构,分别为Node节点和等待队列,其中等待队列为Node节点构成的一个双向列表。
(1)AQS规定了一套机制是当线程获取锁失败之后,会将线程信息封装在一个Node节点中,Node节点信息包含线程信息、要获取什么锁、线程当前处于什么状态等。
(2)然后将Node节点放入等待队列的尾部,让线程继续在等待队列中等待。
(3)同时针对于等待队列有一套机制,规定了等待队列的哪个位置的节点可以重试获取锁,以及针对共享锁怎么在等待队列中的节点进行共享锁传播等。
3.3.1 Node节点
AQS作为一个框架,管理一些获取锁失败之后的线程,放在等待队列中,所以这些线程的一些基本数据它肯定是需要知道的,所以就设计了一个Node的类来管理这些线程的基本数据。下面我们先来看一下Node节点有什么:
static final class Node { // 共享锁模式,表示这个节点的线程要获取的是共享锁 static final Node SHARED = new Node(); // 独占锁模式,表示这个节点的线程要获取的是独占锁 static final Node EXCLUSIVE = null; // 节点状态,CANCELLED表示被取消 static final int CANCELLED = 1; // SINGAL节点,表示下一个节点等待自己唤醒 static final int SIGNAL = -1; // 处于CONDTION模式 static final int CONDITION = -2; // 处于共享锁的传播模式 static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter; }
thread:表示当前节点的线程
prev:前一个等待节点
next:后一个等待节点
锁模式:SHARED表示等待节点要获取的是共享锁、EXCLUSIVE表示等待节点要获取的是独占锁
waitStatus节点的等待状态:
- CANCELLED(1):表示当前节点的线程已经被timeout超时或者中断了,处于该状态的节点不再去获取锁
- SIGNAL(-1):表示后继节点需要当前节点唤醒,当该节点释放锁的时候发现自己的状态是-1的时候,需要唤醒下一个还在等待的线程。
- CONDITION(-2):表示当前节点正在等待一个Condition条件,具体我们后面讲到ReentrantLock的Condition的时候再去剖析
- PROPAGATE(-3):传播模式,在获取共享锁的时候,如果资源还有剩余,发现是-3传播模式,需要唤醒后续的节点。
上面的节点状态 waitStatus < 0 表示节点处于有效状态,waitStatus > 0 表示被取消了,无效。所以啊,AQS中很多情况也是直接使用waitStatus < 0 判断节点是否有有效。
nextWaiter:表示下一个也在等待Condition条件的节点,我们剖析Condition的时候再去讲
也就是当线程获取锁失败的时候,线程会被封装成一个Node节点,然后放入到等待队列的尾部。然后这个Node节点就记录这个线程的一些基本信息,比如这个线程对象、当前要获取的锁是SHARED(共享)的还是EXCLUSIVE(独占)、线程的等待状态是什么(也就是当前线程获取资源之后需要干什么)释放锁需不需要唤醒后续的线程,如果是共享锁需不需要将资源传播下去),等等的这些基本的数据。
3.3.2 等待队列
AQS中使用Node节点存储线程的基本数据,然后一个个的Node节点连接起来就形成了一个双向链表,也就是等待队列。AQS使用两个指针来对这个等待队列进行管理,分别为:
- Node head: head表示等待队列的头结点
- Node tail: tail表示等待队列的尾节点
等待队列的结构画张图说明一下:
接下来我们结合上面讲过的acquire作为获取独占锁入口、tryAcquire实际获取实现、等待队列讲解一下获取锁失败的流程:
(1)首先线程根据AQS提供的获取acquire入口去获取独占锁
(2)acquire调用子类的tryAcquire方法尝试去获取锁,如果获取锁成功直接返回
(3)如果获取锁失败,则将当前线程封装成一个Node节点,放入等待队列中等待
后面在对AQS进行底层的实现机制、源码剖析的时候都会一个一个讲解的;对AQS提供的每个机制都深入到源码级别的剖析。本节我们先整体上让你知道什么是AQS,内部有什么东西,整体上提供了哪些机制和功能,至于底层的实现和分析环节,我们在后面的章节会慢慢讲解的。
3.4 Condition沉睡唤醒机制
之前看Synchronized的时候讲过synchronized的wait和notify机制,wait和notify是控制线程之前沉睡和唤醒机制的,这个必须是整合synchronized一起使用的,因为底层依赖于monitor的waitset集合,AQS作为一个并发的基础框架,它也是提供了类似wait、notify的一套机制,这套机制就是通过Condition来实现的。
condition的await方法就类似之前Object对象的wait()方法一样,具有一样的功能。会让线程释放锁,然后陷入沉睡等待,等待别的线程将它唤醒
condition的singal方法类似之前讲过Object对象的notify()方法,随机唤醒一个因为调用这个condition的await而陷入等待的线程;
而singalAll() 方法就类似于notifyAll方法,会唤醒所有因为调用这个condition的await而进入等待的线程。
![](https://img2023.cnblogs.com/blog/990230/202304/990230-20230405162005606-763270931.png)
AQS提供了的await、singal功能和synchronized体系的wait、notify是一样的,作用也是一样的。至于具体的实现,源码的剖析,我们后边会在一个一个讲解。
4 小结
(1)首先AQS是一个JDK提供的一个并发的基础框架,既然是框架它内部肯定定义了一些通用性的机制和功能,方便上层直接基于AQS开发出各种各样的并发工具。
(2)作为并发框架,它底层定义了一个volatile int state变量作为资源,state具体表示什么资源,独占资源还是共享资源,这个是由子类去定义的
(3)它定义了获取资源的入口,比如获取独占资源入口acquire;获取共享资源入口acquireShared等
(4)定义了一个获取资源的实际方法,独占锁是tryAcquire,共享锁是tryAcquireShared()方法,AQS没有实现,具体获取资源的实际逻辑是由子类实现的,这样子类就能根据自己的实现形成各种各样的并发工具了。
(4-1)同时啊,在获取资源的入口方法使用了模板方法模式,比如acquire方法,里面就定义了一套流程模板;
(4-2)先调用子类的tryAcquire方法获取资源,如果成功直接返回;
(4-3)如果失败则继续走定好的模板流程,先将线程信息封装在Node节点进入等待队列;
(4-4)然后在等待队列里面根据定义好的流程是获取锁还是沉睡等待唤醒
(5)讲解了Node节点的节点状态waitStatus是什么意思,Node节点封装了等待线程的什么数据,同时AQS使用一个等待队列管理这些获取锁失败的线程
(6)还讲解了AQS定义了一套Condition机制,类似于synchronized体系中的wait和notify功能。
接下来就是AQS每个机制的底层是怎么实现的了,我们会深入到源码去剖析,底层是怎么实现的,有理解不对的地方欢迎指正哈。