首页 > 编程语言 >[Java并发]AQS详解之二

[Java并发]AQS详解之二

时间:2024-08-02 17:06:24浏览次数:8  
标签:Node node 结点 Java AQS 获取 详解 线程 资源

参考资料

Java并发之AQS详解
一、概述
  谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!

  类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch...。

  以下是本文的目录大纲:

概述
框架
源码详解
简单应用
  若有不正之处,请谅解和批评指正,不胜感激。

  请尊重作者劳动成果,转载请标明原文链接(原文持续更新,建议阅读原文):http://www.cnblogs.com/waterystone/p/4920797.html

二、框架

  它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:

getState()
setState()
compareAndSetState()
  AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

  不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
  以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

  再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

  一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

三、源码详解
  本节开始讲解AQS的源码实现。依照acquire-release、acquireShared-releaseShared的次序来。

3.0 结点状态waitStatus
这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

0:新结点入队时的默认状态。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。

3.1 acquire(int)
  此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:

1 public final void acquire(int arg) {
2 if (!tryAcquire(arg) &&
3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4 selfInterrupt();
5 }

  函数流程如下:

tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
  这时单凭这4个抽象的函数来看流程还有点朦胧,不要紧,看完接下来的分析后,你就会明白了。就像《大话西游》里唐僧说的:等你明白了舍生取义的道理,你自然会回来和我唱这首歌的。

3.1.1 tryAcquire(int)
  此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。这也正是tryLock()的语义,还是那句话,当然不仅仅只限于tryLock()。如下是tryAcquire()的源码:

1 protected boolean tryAcquire(int arg) {
2 throw new UnsupportedOperationException();
3 }

  什么?直接throw异常?说好的功能呢?好吧,还记得概述里讲的AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现吗?就是这里了!!!AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)!!!至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。

  这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。说到底,Doug Lea还是站在咱们开发者的角度,尽量减少不必要的工作量。

3.1.2 addWaiter(Node)
  此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。还是上源码吧:

复制代码
1 private Node addWaiter(Node mode) {
2 //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
3 Node node = new Node(Thread.currentThread(), mode);
4
5 //尝试快速方式直接放到队尾。
6 Node pred = tail;
7 if (pred != null) {
8 node.prev = pred;
9 if (compareAndSetTail(pred, node)) {
10 pred.next = node;
11 return node;
12 }
13 }
14
15 //上一步失败则通过enq入队。
16 enq(node);
17 return node;
18 }
复制代码
不用再说了,直接看注释吧。

3.1.2.1 enq(Node)
  此方法用于将node加入队尾。源码如下:

复制代码
1 private Node enq(final Node node) {
2 //CAS"自旋",直到成功加入队尾
3 for (;

标签:Node,node,结点,Java,AQS,获取,详解,线程,资源
From: https://www.cnblogs.com/DCFV/p/18339147

相关文章

  • java集合的三种遍历方式
    一、迭代器遍历 在遍历过程中,想删除元素可以使用迭代器遍历ps:遍历过程中不能用集合的方法进行遍历,可以用指针的remove方法进行遍历二、增强for遍历idea快捷方式集合名字.for加回车。方法的底层也是利用迭代器三、lambda表达式遍历完整格式简单格式......
  • 程序员进阶架构知识体系、开发运维工具使用、Java体系知识扩展、前后端分离流程详解、
    场景作为一名开发者,势必经历过从入门到自学、从基础到进阶、从学习到强化的过程。当经历过几年企业级开发的磨炼,再回头看之前的开发过程、成长阶段发现确实是走了好多的弯路。作为一名终身学习的信奉者,秉承持续学习、持续优化的信念。不惜耗费无数个日日夜夜,耗费大量时间精力......
  • JAVA后端拉取gitee仓库代码项目并将该工程打包成jar包
    公司当前有一个系统用于导出项目,而每次导出的项目并不可以直接使用,需要手动从gitee代码仓库中获取一个模板代码然后将他们整合到一起它才是一个完整的项目,所以目前我的任务就是编写一个java程序可以自动地从gitee仓库拉取下来那个模板代码到指定地路径上去。并且我还要将这个ja......
  • JAVA中实现队列和栈(Deque接口和ArrayDeque类)
    用什么来实现队列和栈首先JAVA中有一个Queue接口,用来实现队列。Deque其实就是双端队列,代表两端都可进可出的队列。ArrayDeque就是用数组来实现这个双端队列。(Deque由于是接口,只可以用于声明对象,但是没办法实例化,实例化还是要使用ArrayDeque类)这时可能就会产生疑惑,队列有了,......
  • Java SE核心技术——8继承
    继承是面向对象的三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义,追加属性和方法。继承是指在原有类的基础上,进行功能扩展,创建新的类型。继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模。JAVA中类只有单继承,没有多继承!继承是类和类之间的......
  • Java SE核心技术——6类与对象
    面向对象编程(Object-OrientedProgramming,简称OOP)和面向过程编程(Procedure-OrientedProgramming)是两种不同的编程范式,它们在设计和实现软件时采用了不同的方法和哲学。一、面向对象编程核心概念:面向对象编程的核心是"对象",对象可以包含数据(属性)和代码(方法)。万物皆对象。封......
  • Java SE核心技术——7封装
    封装的概述对外部隐藏内部细节1、封装的目的是隐藏对象的内部状态和实现细节,只暴露出一个可以被外界访问和操作的接口。通过将类的属性设置为私有(private),防止外部直接访问和修改这些属性。2、好处:高内聚低耦合(面向对象设计的最高原则)(1)隐藏事物的实现细节降低使用难度(2)提高了......
  • java的数据类型之基本类型
    强类型语言要求变量的使用要严格符合规定,所有变量都必须先定义后使用。如果没有按照指定要求使用变量,则该变量将报错。java就是强类型语言。java的两大数据类型1.基本类型2.引用类型其中基本类型分为八小种按照图示内容了解各个类型的字节范围(若超过该范围,则IDEA会报错)1......
  • 基于SpringBoot的智能购房推荐系统-09040(免费领源码)可做计算机毕业设计JAVA、PHP、爬
    Springboot智能购房推荐系统摘 要近年来随着我国经济的高速发展,房地产业也随之蓬勃发展,尤其是最近国家新出台的房改政策。鼓励居民购房,这对房产公司无疑是一个极好的发展势头。尤为重要的是,近几年随着信息技术和电子商务的快速发展,许多企业都开发了自己房产信息软件。智......
  • java之WIFI信号模块
    开发步骤分为以下几点:1.在AndroidManifest中声明相关权限(网络和文件读写权限)声明权限:<uses-permissionandroid:name="android.permission.ACCESS_WIFI_STATE"/><uses-permissionandroid:name="android.permission.CHANGE_WIFI_STATE"/><uses-permissiona......