队列同步器(AQS)的设计原理

扫描下方二维码或者微信搜索公众号菜鸟飞呀飞,便可关注微信公众号,阅读更多Spring源码分析Java并发编程文章。编程

微信公众号

1. 前言

  • 在Java中锁所能够分为两大类,一类是经过synchrinized关键字实现的隐式锁,一类是JUC包的锁。前者是经过JVM实现的,后者是根据队列同步器(AQS)实现的,也就是今天的主角。
  • 在JUC包下实现了不少锁以及工具类,例如ReentrantLock、ReadWriteLock、CountDownLatch、CyclicBarrier等,均是经过队列同步器实现的,因此理解了队列同步器的实现原理,对使用这些锁及工具类或者阅读这些类的源码会有很大帮助。

2. 什么是AQS

  • 队列同步器的全称是AbstractQueuedSynchronizer,简称AQS,翻译过来就是抽象的队列同步器。从命名就能猜出,这个类是一个抽象类,且是基于队列来实现的一个同步器。JUC包下全部的锁都是基于它来实现的。在AQS中定义了一个int类型的变量:state,用它来表示同步状态,哪一个线程成功对state变量进行了加1操做,那么这个线程就持有了锁;AQS中还定义了一个FIFO(先进先出)的队列,用来表示等待获取锁的线程。
  • 在计算机领域,锁的实现均可以用管程模型来实现。管程模型的示意图以下,在管程模型中存在两个概念:入口等待队列和条件等待队列。既然锁均可以来管程来实现,那么JUC包下实现的锁中是否是也存在这入口等待队列条件等待队列呢?答案是确定的。AQS中也存着两个队列:同步队列条件等待队列,它们分别对应管程中的入口等待队列条件等待队列。今天先分析AQS中的同步队列的数据结构和实现原理,关于AQS中条件等待队列会在Condition类的源码分析中讲解。
  • 关于管程的介绍能够参考这篇文章:管程:并发编程的基石。也能够阅读极客时间上《Java并发编程实战》一课中的第一部分第8讲:管程:并发编程的万能钥匙 【图】

3. AQS中的方法

  • AQS类提供了不少方法,既然是一个抽象类,就会有方法须要子类去重写。AQS中有以下方法须要子类重写。
方法 做用
protected boolean tryAcquire(int arg) 独占式尝试获取锁
protected boolean tryRelease(int arg) 独占式尝试释放锁
protected int tryAcquireShared(int arg) 共享式尝试获取锁
protected boolean tryReleaseShared(int arg) 共享式尝试释放锁
protected boolean isHeldExclusively() 当前线程是否独占式的占用锁
  • 上面子类重写的方法中,获取或者释放锁时都会尝试去修改同步状态state的值,在AQS中提供了三个和同步状态相关的方法。(上面的方法说明中都是用尝试二字,这是由于调用这些方法不必定能获取锁成功或者释放锁成功)
方法 做用
int getState() 获取同步状态state的值
void setState(int newState) 修改同步状态。一般是只有已经获取到锁的线程才调用这个方法去修改同步状态,这个时候由于只有一个线程能取到锁,因此不用担忧并发问题
boolean compareAndSetState(int expect, int update) 经过CAS的方式去修改同步状态,当多个线程同时尝试修改state时使用,它能保证只有一个线程能修改为功
  • AQS的设计采用了模板设计模式,它定义了不少模板方法,在模板方法中会调用由子类重写的方法。这样就抽象出了锁实现的通用逻辑,而针对不一样类型的锁的实现,只须要有给不一样类型锁的同步组件在重写的方法中实现本身特有的逻辑便可。下面列出部分模板方法及其做用。
方法 做用
void acquire(int arg) 独占式获取同步状态,若是线程成功获取了同步状态,则方法会返回,若是没有获取到同步状态,那么当前线程就会进入到同步队列中,并阻塞。该方法对中断没法响应
void acquireInterruptibly(int arg) throws InterruptedException acquire()方法同样,不过该方法能响应中断
boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException acquireInterruptibly()方法的基础上增长了超时限制,当指定时间内若是没有获取到同步锁,就会返回false。
void acquireShared(int arg) 共享式获取同步状态,若是线程成功获取到了同步状态,那么方法就会返回。不然进入到同步队列中进行等待,并阻塞。它与acquire()的区别是,该方法能容许多个线程同时获取到锁
void acquireSharedInterruptibly(int arg) throws InterruptedException acquireShared()方法的基础上增长了响应中断的功能
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException acquireSharedInterruptibly()基础上增长了超时功能,在指定时间内若是没有获取到锁,就会返回false
boolean release(int arg) 独占式释放锁
boolean releaseShared(int arg) 共享式释放锁
  • 从上面的方法中能够发现,这几个模板都是成对出现的,独占式和共享式获取锁,能响应中断的独占式和共享式获取锁,能超时的独占式和共享式获取锁,释放独占式和共享式锁。因此实际上咱们只须要弄明白acquire()方法和release()方法便可,其余的方法与这两个方法的实现几乎同样,只是改变了部分逻辑。
  • 独占式和共享式的区别:独占式表示的是只能有一个线程获取到锁,而共享式表示的是同一时刻容许有多个线程获取到锁。前者的实际应用有ReentrantLock,后者的实际应用有ReentrantReadWriteLock、CountDownLatch、CyclicBarrier等。这些类的源码以及实现原理后面会有文章专门分析。

4. 同步队列的设计原理

要想读懂AQS的源代码,首先须要明白它的设计原理,不然很难看明白其中的逻辑。毕竟代码只是具体实现的工具,编程语言能够多变,但设计原理是不变的。设计模式

4.1 数据结构

  • AQS中两大核心:同步状态同步队列。同步状态由state这个int类型的全局变量实现,哪一个线程成功修改了state的值,就表示这个线程获取到了锁或者释放了锁。同步队列是一个遵循先进先出(FIFO)的队列,它是一个由Node节点组成的双向链表。每个线程在获取同步状态时,若是获取同步状态失败,就会将当前线程封装成一个Node,而后将其加入到同步队列中。Node是AQS里面的一个静态内部类,Node这个数据结构中,包含了5个属性,每一个属性的功能以下列表。Node就是经过这5个属性来实现同步队列等待队列的,关于等待队列今天先暂时不分析,后面在分析Condition源码时会详细分析。
属性名 做用
Node prev 同步队列中,当前节点的前一个节点,若是当前节点是同步队列的头结点,那么prev属性为null
Node next 同步队列中,当前节点的后一个节点,若是当前节点是同步队列的尾结点,那么next属性为null
Node thread 当前节点表明的线程,若是当前线程获取到了锁,那么当前线程所表明的节点必定处于同步队列的队首,且thread属性为null,至于为何要将其设置为null,这是AQS特地设计的。
int waitStatus 当前线程的等待状态,有5种取值。0表示初始值,1表示线程被取消,-1表示当前线程处于等待状态,-2表示节点处于等待队列中,-3表示下一次共享式同步状态获取将会无条件地被传播下去
Node nextWaiter 等待队列中,该节点的下一个节点
  • 经过Node的prev属性和next属性就构成了一个双向链表,也就是AQS中的同步队列,可是想要经过这个队列找到队列中的每个元素,咱们就须要知道这个队列的头结点是谁,尾结点是谁。所以AQS中又提供了两个属性:headtail,这两个属性的类型均是Node类型,它们分别指向同步队列中的头结点和尾结点。这样AQS就能经过head和tail,找到队列中的每个元素。同步队列的结构示意图以下。

同步队列示意图

4.2 实现原理

  • 当一个线程调用acquire()方法获取同步状态的时候,若是此时能成功获取到同步状态,那么就直接返回;若是不能获取到同步状态,此时就表示同步状态已经被其余线程获取到了,那么这个时候,当前线程就须要开始等待,那么如何实现等待呢?此时当前线程先现将本身封装成一个Node,而后这个Node加入到同步队列中。在加入到同步队列以前,须要判断队列有没有被初始化,即队列中有没有节点存在。若是head=null则表示当前同步队列尚未初始化,因此这个时候当前线程作的第一件事,就是初始化队列。如何初始化呢?当前线程须要先初始化head节点,所以它会new一个Node,而后将这个Node赋值给head,注意head节点表示的是获取到同步状态的线程。接着当前线程再将本身封装成一个Node,而后将head的next属性指向这个Node,这样就将本身加入到了队列中。注意head节点的thread属性始终都是null,由于head节点是当前线程建立的,而当前线程只知道有线程获取到了同步状态,可是殊不知道是谁获取到了,因此此时当前线程在初始化head节点的时候,只能让head节点的thread属性为null。当前线程再将本身加入到队列以后,还须要将tail指向本身。在设置head属性和tail属性时,因为存在多个线程并发的可能,因此须要使用AQS提供的compareAndSetHead()、compareAndSetTail()方法,这两个方法会调用Unsafe类的CAS方法,能保证原子性。节点加入到同步队列的示意图以下。

节点加入到队列示意图

  • 同步队列中首节点表示的是获取到同步状态的线程,当首节点表明的线程释放了同步状态,因为AQS遵循FIFO,因此此时线程在释放同步状态后还须要唤醒后面节点的线程去获取同步状态。当有线程获取到同步状态后,须要将本身表明的节点设置为同步队列的首节点。因为此时确定只有一个线程获取到同步状态,所以此时在更新head属性时,不须要经过CAS方法来保证原子性,只须要使用setHead()方法便可。首节点的设置的示意图以下。

首节点设置示意图

5. 总结

  • 本文主要介绍了AQS中各类API的做用,以及分类。最后经过对Node的数据结构分析了AQS的设计原理。
  • 下一篇文章将结合具体的源码来分析AQS是如何实现锁。

微信公众号
相关文章
相关标签/搜索