面试官:谈谈你对AQS的理解

面试官:谈谈你对AQS的理解AQS是java并发包中同步类组件的实现基础,并发包中很多组件都是基于AQS实现的。比如:ReentrantLock、ReentrantRead

欢迎大家来到IT世界,在知识的湖畔探索吧!

AQS是java并发包中同步类组件的实现基础,并发包中很多组件都是基于AQS实现的。比如:ReentrantLock、ReentrantReadWriteLock、CountDownLatch、CyclicBarrier等。因此为了更好地明白并发包中各个同步组件的实现原理,就需要先对AQS有一定的了解,本篇文章我们就一起了解什么是AQS。

本篇文章主要涉及以下内容:

  • 什么是AQS?
  • AQS为什么要设计成抽象类?
  • AQS中的队列是什么?
  • AQS如何保证队列线程安全的?

什么是AQS?

AQS的全称是AbstractQueuedSynchronizer。先让我们从AbstractQueuedSynchronizer这类名中认识一下AQS是什么?首先是Abstract,Abstract是抽象的意思,我们知道java中被abstract修饰的类是抽象类,由此可知,AQS是一个抽象类;接下来从Queued这个单词我们可以知道AQS也是一个队列,队列的特性是先进先出,因此可以从这里可以知道AQS存储的数据是有序的;最后是Synchronizer,这个是同步的意思,同步意味着线程安全,也就是说AQS是线程安全的;将三个单词连起来也就是说AQS的是一个线程安全的抽象的同步队列,因此AQS也被称为抽象同步队列

我觉得要搞明白AQS是什么,就要搞清楚以下问题:

  • AQS为什么要设计成抽象类?
  • AQS中的队列是什么?
  • AQS如何保证队列线程安全的?

AQS为什么要设计成抽象类?

我们首先看一下AQS在java中是怎么定义的,代码如下所示:

//AQS被abstract关键字修饰,因此AQS是一个抽象类 public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { ....省略代码实现 }

欢迎大家来到IT世界,在知识的湖畔探索吧!

从上面的代码中我们可以看出,AQS被abstract关键字修饰,因此AQS是一个抽象类。在java中抽象类是不能被实例化的,也就是不能直接对抽象类执行new操作来创建对象,抽象类只能被其它子类用来继承,子类继承抽象类后,可以基于抽象类提供的模板方法来扩展自己的实现功能,这也是模板方法模式的实现。

模板方法模式:在父类的方法中定义一个算法的整体执行流程,并提供扩展的方法供子类进行扩展实现。子类在不修改算法整体执行流程的前提下,可以基于扩展的方法定制化算法执行流程,以到达功能扩展和代码复用的目的。

AQS被设计为了抽象类,这也就说明了AQS不能被直接使用,只能被子类来继承实现扩展功能,子类可以基于AQS提供的基础功能来扩展实现自己的特定功能逻辑,这也就是为什么AQS是其它同步类实现的基础,比如:ReentrantLock基于AQS扩展实现了锁功能,CountDownLatch基于AQS扩展实现了多线程的协调功能等等。

既然AQS为其它同步类提供了基础的实现,那么它具体提供了什么实现呢?下面我们就来看看AQS中的队列。

AQS中的队列是什么?

AQS中的队列是使用双向链表实现,当往队列中添加元素时会将元素封装为Node节点添加到链表的尾部,当从队列中取元素时会从链表头部取出元素,从而实现了队列的先进先出特性,下面我们来看一下在AQS中的队列是如何定义的?

欢迎大家来到IT世界,在知识的湖畔探索吧!//AQS抽象类 public abstract class AbstractQueuedSynchronizer { //head节点指向链表的头节点 //队列元素出队时操作的是链表头结点 private volatile Node head; //tail节点指向链表的尾结点 //队列元素入队时操作的是链表尾节点 private volatile Node tail; //链表中的节点实现,双向链表 static final class Node { //指向上一个节点 volatile Node prev; //指向下一个节点 volatile Node next; //节点保存的数据,是Thread线程 volatile Thread thread; ...省略其它属性和代码 } }

在AQS中持有双向链表的head指针和tail指针,分别指向链表的头结点和尾结点,当往链表中添加元素时操作的是tail指针,将新添加的元素添加到尾结点后面;当从链表中取出元素时,操作的是head指针,从链表头部取出节点数据,通过这种方式来保证队列的先进先出特性;在链表中的每个Node节点中持有prev和next两个指针分别指向节点的前驱节点和后继节点,从而将整条链表串联为双向链表。

细心的朋友们可以发现,在AQS中的head、tail指针,Node节点中的prev、next和thread变量都使用volatile关键字修饰,这是为了保证共享变量的内存可见性,关于volatile如何保证内存可见性的问题,可以参考我之前的文章《面试官:讲一讲你对volatile的理解》,这里就不展开讲述了。这里只要知道使用volatile关键字修饰共享变量的作用是为了保障共享变量在多线程操作时的内存可见性问题,从而保证了对共享变量的操作是线程安全的。

在上面的Node节点的定义中我们发现了一个Thread类型的成员变量thread,这也就是说存放到链表中节点的数据是线程,那么Node节点中要存放线程呢?

在文章的开头我们提到,AQS是实现同步类工具实现的基础,很多同步类工具底层都是依靠AQS实现的,比如:ReentrantLock、CountDownLatch等。这些同步类工具本质上是为了协调多个线程间的操作。例如:ReentrantLock是为了保证多线程执行临界代码的原子性,CountDownLatch是为了实现一个线程等待多个线程执行的效果,可以看到这些同步类操作的对象都是线程,因此在AQS中就需要将这些线程保存到队列中,以便对这些线程实现细粒度的管理,比如挂起线程、唤醒线程、对多个线程进行排队等。具体要对线程执行什么操作,是由子类要实现的具体功能决定的,关于更多同步类工具的实现,可以关注小编,小编会在后续的文章中分享。

AQS如何保证队列线程安全的?

在小编之前的文章中提到过,要保证线程安全就需要保证多线程操作的原子性和共享变量的内存可见性(关于线程安全的问题可以查看小编之前发布的文章内容),换句话说就是如果我们要搞清楚AQS中的队列是如何保证线程安全,只需要知道AQS中是如何保证队列的操作的原子性和内存可见性即可。

我把前面的AbstractQueuedSynchronizer的代码贴到下面方便查看。

//AQS抽象类 public abstract class AbstractQueuedSynchronizer { //head节点指向链表的头节点 //队列元素出队时操作的是链表头结点 private volatile Node head; //tail节点指向链表的尾结点 //队列元素入队时操作的是链表尾节点 private volatile Node tail; //链表中的节点实现,双向链表 static final class Node { //指向上一个节点 volatile Node prev; //指向下一个节点 volatile Node next; //节点保存的数据,是Thread线程 volatile Thread thread; ...省略其它属性和代码 } }

AQS是如何保证内存可见性的?

从之前的内容和上面的代码中,我们知道AQS中的队列是使用双向链表实现的,AQS中持有的链表head和tail节点都是使用volatile关键字进行修饰,并且链表中的节点Node使用的变量也都是使用volatile关键字进行修饰,综上所述我们可以知道,AQS中保证队列的内存可见性是通过volatile关键字来保证的。

AQS是如何保证队列操作的原子性的?

我们知道队列主要操作是入队和出队,入队操作是在队列尾部插入节点,而出队操作是从队列头部移除节点。

我们首先看一下AQS中是如何保证队列的入队操作是原子性的?AQS中队列入队操作是在enq方法中实现,代码如下所示:

欢迎大家来到IT世界,在知识的湖畔探索吧!//队列元素出队时操作的是链表头结点 private volatile Node head; //tail节点指向链表的尾结点 //队列元素入队时操作的是链表尾节点 private volatile Node tail; //enq方法,将node节点插入到链表尾部,也就是队列尾部 private Node enq(final Node node) { for (;;) { //无限for循环 //获取尾节点 Node t = tail; if (t == null) { //尾节点为空,则说明队列还没有初始化,需要执行初始化 //注意这里传入的是new Node节点,也就是一个哨兵节点,用于简化链表编程 if (compareAndSetHead(new Node())) tail = head; //通过cas设置head节点后,将tail指向head,此时tail和head指向哨兵节点 } else { //将node节点插入到双向链表尾部 node.prev = t; if (compareAndSetTail(t, node)) { //cas设置tail节点为新插入的节点 t.next = node; return t; //cas成功,则返回 } //cas设置tail节点失败,则重新for循环重新将node节点插入到tail节点后面,直到成功退出 } } } //通过Unsafe设置head节点,cas操作,只有一个线程可以设置成功 //参数update为要设置的头节点 private final boolean compareAndSetHead(Node update) { //如果cas操作时,head执行null,则将head指向传入的update节点 return unsafe.compareAndSwapObject(this, headOffset, null, update); } //通过Unsafe设置tail节点,cas操作,只有一个线程可以设置成功 private final boolean compareAndSetTail(Node expect, Node update) { //cas设置tail节点 return unsafe.compareAndSwapObject(this, tailOffset, expect, update); }

从上面的enq代码中,我们可以了解到AQS是基于无限for循环+cas来保证入队操作的原子性。在for循环中首先判断队列是否已经进行了初始化,如果队列还没有初始化,则使用cas操作将head指针指向一个空的哨兵节点Node,然后将tail也指向head节点,也就是说对链表进行初始化的时候head和tail指针都指向哨兵节点Node,设置哨兵节点主要是为了简化链表的操作,哨兵节点本身不存储数据,无其它意义;当队列初始化后,就将使用cas操作将要添加的node节点插入到队列尾部,因为cas操作保证只会有一个线程能执行成功,执行失败的线程会重新执行for循环进行重试,以此来保证入队操作的原子性。

需要注意的是在AQS中并没有保证队列出队操作的原子性,因为AQS中出队操作只有在特定操作下才会执行。比如在ReentrantLock中,只有在获取到锁资源的线程才能执行AQS的出队操作,而获取同一个时间只能有一个线程获取到锁资源,因此AQS的出队不存在线程安全问题,也就不需要原子性的保证。关于ReentrantLock的更多知识可以关注小编,小编将在后续的内容中进行分析。

总结

本篇文章中只是针对AQS中与队列相关内容进行介绍,AQS的内容还不止这些,比如对状态值state的操作等,但由于这些操作都与具体子类(同步类组件)的实现逻辑有关,因此没有在本篇文章中进行介绍。

关于AQS的其它内容,可以关注小编,小编将在后续介绍同步组件实现的时候进行讲解。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/72707.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们YX

mu99908888

在线咨询: 微信交谈

邮件:itzsgw@126.com

工作时间:时刻准备着!

关注微信