当前位置:网站首页 > Java基础 > 正文

java高并发编程基础之aqs



Java JUC(三) AQS与同步工具详解

一. ReentrantLock 概述

是 包下的一个同步工具类,它实现了 接口,提供了一种相比关键字更灵活的锁机制。 是一种独占式且可重入的锁,并且支持可中断、公平锁/非公平锁、超时等待、条件变量等高级特性。其特点如下:

  • 独占式: 一把锁在同一时间只能被一个线程所获取;
  • 可重入: 可重入意味着同一个线程如果已持有某个锁,则可以继续多次获得该锁(注意释放同样次之后才算完全释放成功);
  • 可中断: 在线程获取锁的等待过程中可以中断获取,放弃等待而转去执行其他逻辑;
  • 公平性: 支持公平锁和非公平锁(默认)两种模式。其中,公平锁会按照线程请求锁的顺序来分配锁(降低性能),而非公平锁允许线程抢占已等待的线程的锁(可能存在饥饿现象);
  • 条件变量: 通过 接口的实现,允许线程在某些条件下等待或唤醒,即可以实现选择性通知;
TypeMethodDescription/无参构造方法,默认为 非公平锁/带参构造方法,其中表示锁的公平性策略:
- : 公平锁
- : 非公平锁不可中断式获取锁。若当前锁已被其他线程持有,则阻塞等待;注意:该获锁过程不可被中断可中断式获取锁。若当前锁已被其他线程持有,则阻塞等待;注意:该获锁等待过程可被中断,抛出,并清除当前线程的中断状态尝试获取锁,该方法会立即返回。若获锁成功,则返回,否则将返回;注意:该方法会破坏公平锁配置,即在公平锁策略下,该方法也会立即尝试获取可用锁在给定时间内尝试获取锁。若获锁成功,则返回,否则将阻塞等待直到过期,返回; 注意:
- 获锁等待过程可被中断,抛出,并清除当前线程的中断状态
- 遵循公平锁配置策略,即在公平锁策略下,该方法会按顺序等待获取锁当前线程尝试释放该锁。若当前线程未持有该锁,则抛出异常返回一个与当前实例绑定的条件变量集合对象(默认返回内部实现类),用于实现线程的条件等待/唤醒(详见后文)

1. 锁的基本使用

相比关键字来说,属于显式锁,其锁机制都是针对实例对象本身进行加锁,并且在使用过程中需要手动释放,即锁的获取与释放是成对出现的;除此之外,属于JDK API层面实现的互斥锁,其通过方法调用实现锁功能,可以跨方法从而更加灵活。为了避免出现死锁问题,官方建议的开发方式如下:

 

问题背景: 假设当前有一个卖票系统,一共有100张票,有4个窗口同时售卖,请模拟该卖票过程,注意保证出票的正确性。

 

2. 条件变量机制

在关键字中&java高并发编程基础之aqs#xff0c;我们可以通过实现线程的等待与唤醒,但在此场景下存在虚假唤醒问题,根本原因就是其等待/唤醒机制只支持单条件(等待线程未作区分,只能全部唤醒)。相比之下,基于接口也实现了相同的机制,并提供了更细的粒度和更高级的功能;每个实例都对应一个条件队列,用于维护在该条件场景下等待通知的线程,并且支持多条件变量,即一个可以关联多个实例。其常用方法如下:

TypeMethodDescription使当前线程阻塞等待(进入该条件队列),并 释放与此条件变量所关联的锁。 注意: 若在等待期间被中断,则抛出,并清除当前线程中断状态使当前线程阻塞等待,并 释放与此条件变量所关联的锁,直到被唤醒、被中断或 等待时间过期。其返回值表示:
- : 在等待时间之内,条件被唤醒;
- : 等待时间过期,条件未被唤醒;
注意: 若在等待期间被中断,则抛出,并清除当前线程中断状态唤醒 一个等待在上的线程。如果有多个线程在此条件变量下等待,则选择任意一个线程唤醒; 注意: 从等待方法返回前必须重新获得相关联的锁唤醒 所有等待在上的线程。如果有多个线程在此条件变量下等待,则全部唤醒 ; 注意: 从等待方法返回前必须重新获得相关联的锁

在使用时需要注意

  • 调用相关方法前需要先获得对应条件变量所关联的锁,否则会抛出异常;
  • 调用相关方法前需要先获得对应条件变量所关联的锁,否则会抛出异常;
  • 线程被唤醒(或等待时间过期、被中断)后会重新参与锁的竞争,若成功拿到锁则将从处恢复继续向下执行;
 

二. 从 ReentrantLock 分析 AQS 的原理

1. AQS 框架

全称为 ,即抽象队列同步器; 是 包下的一个抽象类,其为构建锁和同步器提供了一系列通用模板与框架的实现,大部分包下的并发工具都是基于来构建的,比如、、等。其核心源码如下:

 

由上可知,AQS内部实现了一个核心内部类,该内部类表示对等待获取锁的线程的封装节点;在AQS中,基于维护了一个双向链表(模拟同步队列),其中节点指向同步队列的头部,而节点指向同步队列的尾部。类的核心源码如下:

 

在了解的基本数据结构与状态之后,还有一个核心状态变量即,该全局变量用于表示同步锁的状态,其具体含义一般在子类实现中进行定义和维护(独占锁和共享锁不一样)。但不管独占模式还是共享模式,简单来说都是使用一个的全局变量来表示资源同步状态(),并通过CAS完成对值的修改(修改成功则表示获锁成功),当持有锁的线程数量超过当前模式(独占模式一般限制为1)时,则通过内置的FIFO同步队列来完成资源获取线程的排队工作(通过方法实现挂起与唤醒)。其核心原理图如下:

在这里插入图片描述

综上,AQS采用了模板方法模式来构建同步框架,并提供了一系列并发操作的公共基础方法,支持共享模式和独占模式两种实现;但AQS并不负责对外提供具体的加锁/解锁逻辑,因为锁是千变万化的,AQS只关注基础组件、顶层模板这些总的概念,具体的锁逻辑将通过”钩子“的方式下放给子类实现。也就是说,独占模式只需要实现方法、共享模式只需要实现方法,搭配AQS提供的框架和基础组件就能轻松实现自定义的同步工具。

2. ReentrantLock 源码分析

在这里插入图片描述

由的类结构图可以看出,实现了接口,其内部包含一个内部类,该内部类继承了(),大部分的锁操作都是通过实现的。除此之外,有公平锁和非公平锁两种模式,分别对应的和两个子类实现。

 

接下来,本节将首先以的非公平锁为例进行分析,然后再介绍公平锁与非公平锁的主要区别。

2.1 非公平锁lock加锁原理

非公平锁的源码如下:

 

在调用方法获取锁的过程中,当前线程会先通过CAS操作尝试修改从(表示无锁)到(表示占有锁),若修改成功则将中保存的独占线程修改为当前线程;若失败则执行方法,该方法是中的一个模板方法,其源码如下:

 

可以看到该方法首先调用了钩子方法,该方法是交由子类实现的,在上面的源码中我们已经给出了的实现代码,其直接调用了父类中的方法,其源码如下:

 

由上述代码可知,该方法首先再次判断锁是否已释放,这是为了避免之前持锁的线程在这段时间内又重新释放了锁,若则会尝试再次CAS修改同步状态以获取锁资源;否则的话,则判断当前线程是否是锁重入的情况,若两个判断都不满足则返回;到目前为止,我们回过头来想一下非公平锁的非公平性是在哪体现的?

很明显,在上述代码分析中,当有任何线程尝试获取锁时(调用方法),不论当前同步队列中是否已有线程排队等待,的方法以及的方法都没有对同步队列中的等待情况进行判断,而是直接通过CAS尝试修改的值来为当前线程直接占有锁;这就是非公平性的体现,抢占线程可以直接与等待线程竞争锁资源,而不用按照顺序加入队列

分析完这部分之后,我们再回到方法,若方法返回即获取不到锁时会继续向下执行到方法,该方法用于封装线程入队,其源码如下:

 

接着,我们继续分析方法的实现:

 

可以看到头节点其实是不存储数据的,它只表示一个线程占位(占位了锁资源),因为位于头节点的线程肯定已经获取到了锁,头节点只存储后继节点指向,用于当前线程释放锁资源时唤醒后继节点;那么到此这个方法也就分析完成了,在节点入队成功之后会返回当前节点,然后会继续执行到方法,其源码如下:

 

在方法中,线程会开启自旋,若发现(或被唤醒后发现)当前节点的前驱节点变为头节点,则说明当前节点能够尝试获取锁资源,并尝试通过方法获取同步状态;需要注意的是,头节点表示当前占有锁的线程节点,只有当节点对应的线程释放锁资源并唤醒后继节点时,后继节点线程才会自旋去尝试占有锁资源,因此:在同步队列中,只有前驱节点变为头节点时,当前节点才有资格尝试获取锁资源,其他时候都将被挂起等待,避免空转

除此之外,若在自旋过程中,当前节点的前驱节点不是头节点或者节点尝试获取锁资源失败,则会执行逻辑;需要注意的是在前驱节点是头节点但尝试获取锁资源失败这种特殊情况发生时(比如非公平锁模式下被新到来的请求线程抢占),头节点此时可能有两种状态:

  • : 处于该状态一种情况是初始同步队列为空时,默认头节点状态初始化为0;另一种情况是锁释放时(见后文)被重置头节点状态;
  • : 处于该状态一种情况是时更新状态后又自旋回来但仍未获取到锁(可能释放锁后被非公平抢占);另一种情况是释放锁时重置失败;

其源码如下:

 

方法的执行逻辑是判断前驱节点的等待状态,用于挂起当前节点线程等待,结合上述分析,包括以下几种情况:

  • 前驱节点状态: 若前驱节点是头节点,则说明占锁线程还未执行结束,当前节点线程仍需挂起等待;若前驱节点不是头节点,则说明前驱节点就绪/拥有更高的优先级,下次执行还轮不到当前节点,所以也可以安全挂起,直接返回;
  • 前驱节点状态: 说明前驱节点已处于结束/取消状态,应该从同步队列中移除,并遍历所有前驱节点直到找到非结束状态的有效节点作为前驱;
  • 前驱节点状态且非SIGNAL: 前驱节点刚从的条件等待队列被唤醒,从而转移到同步队列,需要转换为状态等待;
  • 前驱节点状态:若前驱节点是头节点,则说明同步队列刚初始化(0)或锁刚被释放重置,锁资源可能未被其他线程持有,需判断能否占有锁(不管当前线程能否占有,该锁一定会被占有,都需要转换状态为);若前驱节点不是头节点,则说明该线程节点刚初始化并**入队列,需要转换为状态;

综上,当方法返回时会调用方法挂起线程等待被唤醒,返回时则会继续自旋判断;至此,内部间接依靠的同步队列,就完成了加锁操作。

2.2 公平锁lock加锁原理

公平锁的源码如下:

 

与非公平锁唯一不同的是:公平锁的实现中,在尝试修改之前,会先调用判断内部同步队列中是否已存在等待节点。如果存在,则说明在此之前,已经有线程提交了获取锁的请求,那么当前线程就会直接被封装成节点,追加到队尾等待。

2.3 释放锁原理

显式锁需要手动释放锁资源,其方法直接调用了中的方法,而该方法又是在其父类中直接实现的,其源码如下:

 

方法能否释放锁并唤醒后继节点线程依赖于钩子方法,而该方法又下放到了中实现,其源码如下:

 

注意:方法执行完成返回之后,就说明当前线程持有的锁已被释放(非公平锁中就已经可以被抢占了),后续方法只是进行一些善后工作,其中重置头节点状态的目的是表示逻辑上从持锁到无锁的转换,锁资源目前可能并没有线程持有,因此在后续线程唤醒后执行自旋时状态会再一次判断并尝试获取锁,而修改为就表示占锁线程正在执行,其他线程需要挂起等待。至此,整个流程可以结合起来理解:节点的线程被唤醒后,会继续执行方法中的自旋,判断代码是否成立,从而执行判断操作。

三. 其他同步工具类

1. Semaphore

1.1 基本概述

是包下的一种计数信号量,它同样也是基于实现的同步工具类。相比来说,它应该属于共享锁,即允许多个线程同时访问某个共享资源,但会限制同时访问特定资源的线程数量;同样也支持公平模式和非公平模式两种方式,其构造方法如下:

 

默认为非公平模式(抢占式),在构造信号量对象时都必须提供参数;可以理解为许可证数量,只有拿到许可证的线程才能执行,该参数限制了能同时获取或访问到共享资源的线程数量,其他超出线程都将阻塞等待。基于此,通常用于实现资源有明确访问数量限制的场景,比如限流、池化等。的常用方法介绍如下:

TypeMethodDescription当前线程请求获取该信号量的一个许可证。若有可用许可证,则获得许可证并返回执行同步代码,同时可用许可证数量将减少一个;若没有可用许可证,则阻塞等待直到有许可被释放,或线程中断。 注意: 若当前线程在等待过程中被中断,则会抛出,并清除当前线程的中断状态。当前线程请求获取该信号量的个许可证。若有可用数量的许可证,则获得许可证并返回执行同步代码,同时可用许可证数量将减少个;若没有可用数量的许可证,则阻塞等待直到可用许可达到指定数量,或线程中断。注意: 若当前线程在等待过程中被中断,则会抛出,并清除当前线程的中断状态。释放该信号量的一个许可证,并使可用许可证数量增加一个。 注意: 没有通过获取许可的线程甚至也可以直接调用来为信号量增加许可证数量,并且可用许可有可能会超出构造时限制的值,因此信号量的正确使用必须是通过应用程序中的编程约束来建立。释放该信号量的个许可证,并使可用许可证数量增加个。 注意: 同方法,信号量的正确使用必须是通过应用程序中的编程约束来建立。尝试获取该信号量的一个许可证,但该方法会立即返回。若有可用许可证,则获得许可证并返回,同时可用许可证数量将减少一个;若没有可用许可证,则返回。 注意: 该方法会破坏公平策略,对该方法的调用会进行抢占式获取(不管是否有线程在等待)。尝试在指定时间内获取该信号量的一个许可证(遵循公平策略)。若有可用许可证,则获得许可证并返回,同时可用许可证数量将减少一个;若没有可用许可证,则阻塞等待直到过期,等待时间过期则返回。 注意: 若当前线程在等待过程中被中断,则会抛出,并清除当前线程的中断状态。获取当前信号量中的可用许可证数量。

可以看出,许可证是的核心概念,信号量对许可证的获取是强限制,但对许可证的释放是弱限制的,即请求线程在执行时必须获取到指定数量的许可证,但在释放时并不会对先前是否获取进行检查,因此可用许可有时可能会超出构造时限制的值。换句话说,构造时传入的参数只表示信号量的初始许可数量,并且许可证只决定了线程执行的门槛,但并不会对线程作全程限制;当前线程一旦获取到指定数量的许可便开始执行,即使中途释放许可也不会影响后续执行过程,这也就是为什么说信号量的正确使用必须是通过应用程序中的编程约束来建立。举例如下:

 
 

前文说过,通常用于实现资源有明确访问数量限制的场景,比如限流、池化等;此处通过模拟一个请求限流的场景,其中限制最大并发数为3,实现代码如下:

 

1.2 原理分析

的大部分方法都是基于内部类实现的,而该类又继承了 即,并且对应的还有两个子类 (非公平模式实现) 和 (公平模式实现)。在中,的 被定义为 (许可证数量),对象创建时传入的参数实际是在对AQS内部的进行初始化,初始化完成后代表着当前信号量对象的可用许可数()。

以非公平模式为例,当线程调用请求获取许可时,会首先判断是否大于0,如果是则代表还有满足可用的许可数,并尝试对进行CAS操作使,若CAS成功则代表获取许可成功;否则线程需要封装成Node节点并加入同步队列阻塞等待,直到许可释放被唤醒。

 
 

释放逻辑对比获取许可的逻辑相对来说要简单许多,只需要更新值增加后调用方法唤醒后继节点线程即可;需要注意的是,而在共享模式中可能会存在多条线程同时释放许可/锁资源,所以在此处使用了的方式保证线程安全问题。

2. CountDownLatch

2.1 基本概述

同样是包下的基于实现的同步工具类。类似于,在初始化时也会传入一个参数来间接赋值给的,用于表示一个线程计数值;不过并没有构建公平模式和非公平模式(内部没有子类实现),其构造方法如下:

 

的主要作用是等待计数值归零后,唤醒所有的等待线程。基于该特性,常被用于控制多线程之间的等待与协作(多线程条件唤醒);相比来说,更加灵活且粒度更细,是以线程执行结束为条件,而是以方法的主动调用为条件。其常用方法如下:

TypeMethodDescription使当前线程阻塞等待,直到计数器归零或线程被中断。若当前计数已为零,则此方法立即返回。 注意: 若在等待过程中被中断,则会抛出InterruptedException,并清除当前线程的中断状态。使当前计数器递减。如果新计数归零,则唤醒所有等待线程; 注意: 若当前计数已为零,则无事发生。获取当前计数器的值。

需要注意的是, 是一次性的,即计数器的值只能在构造方法中初始化,此外再没有任何设置值的方法,当 使用完毕后(计数归零)将不能重复被使用;若需要重置计数的版本,可以考虑使用。 的常用方法有两种:

  • 多等一:初始化,多条线程阻塞等待一条线程调用唤醒所有线程。比如模拟并发安全、死锁等;
  • 一等多:初始化,一条线程阻塞等待N条线程调用归零后唤醒。比如多接口调用的数据合并、多操作完成后的数据检查、主服务启动后等待多个组件加载完毕等(注意线程间的通信与数据传递需结合实现);
 

2.2 原理分析

的底层实现原理也非常简单,当线程调用 的时候,如果 不为 0 则证明任务还没有执行结束, 就会进入阻塞等待,其源码如下:

 

当线程调用 时,其实最终是调用了中重写的方法,该方法以 CAS 的操作来减少 ;若更新后 归零,则表示所有的计数任务线程都执行完毕,那么在 上等待的线程就会被的方法唤醒并继续向下执行。

 

3. CyclicBarrier

  • 上一篇: java零基础数组
  • 下一篇: 只会java基础实习
  • 版权声明


    相关文章:

  • java零基础数组2024-11-15 14:02:03
  • 编程0基础学c还是java2024-11-15 14:02:03
  • java基础教学4012024-11-15 14:02:03
  • java基础教程112024-11-15 14:02:03
  • java怎么搭建一个基础项目2024-11-15 14:02:03
  • 只会java基础实习2024-11-15 14:02:03
  • 大学java基础2024-11-15 14:02:03
  • java 基础题2024-11-15 14:02:03
  • java基础循环的特点2024-11-15 14:02:03
  • android java基础面试2024-11-15 14:02:03