深入浅出,从 ReentrantLock 到 AQS | Java(上)

简介: 对于非Java后端同学来说,没听过倒也不是什么太过分的事,但是如果你深入学习过 Java 并发相关,那么肯定会去了解各种锁,而作为一个 有志青年 的你必然会在心里来一句,为什么加了锁就可以同步 ? 此时必然也会看到 AQS 的影子。

引言

如果问一个 Android 同学,请你简单说一下 Java AQS 的基本思想,那么有不少于一半的同学可能是

什么玩意,AQS 是什么,我咋没听过🙃。

的确,对于非Java后端同学来说,没听过倒也不是什么太过分的事,但是如果你深入学习过 Java 并发相关,那么肯定会去了解各种锁,而作为一个 有志青年 的你必然会在心里来一句,为什么加了锁就可以同步 ? 此时必然也会看到 AQS 的影子。

从技术的角度讲,当我们谈到 ReentrantLock ,不难也会说到 AQS

如果上述所讲你不了解的话,那么本篇文章可能会对你有所帮助。

背景

在了解一项技术之前,我们有必要了解这项技术所产生的原因,及它能为我们带来什么?

AQS 全名 AbstractQueuedSynchronizer,在 jdk1.5 时加入 ,是整个 JUC 的基础,我们现在用到的大多数同步器都是基于 AQS 的,其性能相比于 常用的 synchronized 在一定程度上提高了不少,而它的构建者 也就是并发包的大师 (Doug Lea) 更是期望它能够成为实现大部分同步需求的基础。

那AQS出现原因是什么呢? 难道 AQS 的出现仅仅是为了提升性能吗,或者说仅仅是因为性能,就要重复造一个 AQS 的轮子?

要解释这个问题,首先我们要思考一下在它出现之前,我们经常使用的同步方式是 synchronized ,而 synchronized 是没办法解决死锁,因为其在申请资源时,如果申请不到,线程将直接进入阻塞状态,也即释放不了线程已经占用的资源,而为了解决这个问题,我们就需要新的方案。

如果让我们自己重写去设计一个工具来解决上述问题,那该怎么去设计,AQS提供了以下方案?

支持响应中断 synchronized 一旦进入阻塞状态,就无法被中断。但如果阻塞状态的线程能够响应中断信号,能够被唤醒。这样就破坏了不可被抢占条件了。

支持超时。 如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁,这样也能破坏不可抢占条件。

非阻塞的获取锁。 如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

AQS是什么?

抽象队列同步器 AbstractQueuedSynchronizer ,是用来众多同步组件的一个 基础框架 , 内部用到了 CLH 队列锁 。

如果之前没了解过,那么这句话可能听着有点懵,先别急着搞清原理,我们先在脑海里有个概念 :

这玩意是用来构建锁的基础框架 , 这玩意是用来构建锁的基础框架 , 这玩意是用来构建锁的基础框架

有人说,AQS 只是一个工具类而已,的确如此。但是其的重要性在并发编程中用 框架 这个词或许更为合适。

1. AQS 基础理论入门

AQS 其主要使用方式是继承,即子类通过继承 AQS 实现它的抽象方法来管理状态,内部使用一个 int 成员变量 state 表示同步状态,并且通过一个 FIFO(先进先出) 的队列来完成线程的排队工作。

在具体的实现上,通常子类推荐被定义为静态内部类(就像 ReentrantLock中 的 Sync ),AQS 本身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式的获取同步状态,也可以支持共享式的获取同步状态,这也是 jdk 中内置的同步组件的实现原理。

总览

网络异常,图片无法展示
|

描述一下工作流程:

每次在请求一个资源时,先将其添加到 CLH 队列中,并且 state+1 , 队列处于循环遍历阶段(当 state=0 时,代表资源无占用),队列循环过程中,如果被请求的共享资源空闲状态,则将当前请求资源的线程设置为有效的工作线程,并且更改其node节点中的 locked 为 true ,代表其处于占用状态,后续结点(即后续需要获取资源的线程)就一直处于自旋等待状态,不停的判断前一个节点 的 locaked 是否为 false ,如果为 false ,则证明此时资源处于空闲状态,则去尝试获取资源。

模板方法模式

如果翻阅 ReentrantLock 的源码,会发现一个问题,那就是 除了最开始的 lock() 是 ReentrantLock 本身提供的方法之外,而剩下的方法解析,也就是是其内部的核心方法调用,都是调用了 AQS 的相关方法。

并且都 NonfairSync 实现了 AQS 的一些方法,比如 tryAcquire() ,以便于提供具体的实现逻辑。

而这样的设计我们称之为 模板方法模式 :

即也可以总结为:对于一个设计而言,我们将不容易改变的方法提前定义好具体的实现逻辑,并且将一些需要具体实现的交给外部自己实现。并将这种类似的方法按照我们的逻辑需要,包装成几个通用的方法调用合体,以供外部调用。

  • 对于外部而言,其本身并不知道我内部的具体实现;
  • 外部需要实现自己相应的独特逻辑的几个特定方法;

这样的好处就是 我们封装了不变的部分,仅扩展了可变部分,并且提取了公共代码,以便于维护。

CLH队列锁

CLH队列锁 是一种基于链表的可扩展,高性能,公平的自旋锁,申请资源的线程仅仅在本地变量上自旋,它不断轮训前驱节点的状态,假设发现前驱释放了锁,就结束自旋。

当一个线程需要获取锁时,先创建一个的 QNode ,将其中的 locked 设置 true 表示需要获取锁, myPred 表示对其前驱结点的引用。

网络异常,图片无法展示
|

其完整流程如下所示:

  1. 假设线程A要获取资源,其先使自己成为队列的尾部,同时获取一个指向其前驱结点的引用 myPred,并不断在父节点引用上自旋判断。

网络异常,图片无法展示
|

2.当另一个线程B同样也需要获取锁时,上述的过程同样也要来一遍,如下所示 (QNode-B)

3.当某个线程要释放锁时,就将当前节点的 locked 设置为 false

其后续节点因为不断在自旋,当判断到其前序节点 lockedfalse ,就表明其前序节点已经释放锁,其自身就可以获取到锁,并且释放当前前序节点引用,以便GC回收。

整个过程如上图所示,CLH队列锁 的优点是空间复杂度低,如果有n个线程,L个锁,每个线程每次都只获取一个锁,那么其需要的存储空间 O(L+n) ,n个线程有n个 node,L 个锁有L个tail .

AQS 中的 CLH队列锁 实现方式与上述方式相比是一种变体的实现,相比普通 CLH队列锁 ,AQS 中的实现方式 做了相关的优化,比如不会不断重试,而会在重试相关次数后将线程阻塞。等待之后的唤醒。

AQS 相关方法

模板方法

在我们实现自定义的同步组件时,将会调用同步器提供的模板方法。相关方法如下:

上述模板方法同步器提供的模板方法分为3类:

  1. 独占式获取与释放同步状态
  2. 共享式获取与释放
  3. 同步状态和查询同步队列中的等待线程情况。

可重写的方法

访问或修改同步状态的方法

在自定义的同步组件框架中,AQS 抽象方法在实现过程中免不了要对同步状态 state 进行更改,这时就需要同步器提供的3个方法来进行操作,因为他们能够保证状态的改变是安全的:

getState()

获取当前同步状态

setState(newState:Int)

设置当前同步状态

compareAndSetState(expect:Int,update:Int)

使用 CAS 设置当前状态,该方法能保证状态设置的原子性。

目录
相关文章
|
3月前
|
存储 Java
JAVA并发编程AQS原理剖析
很多小朋友面试时候,面试官考察并发编程部分,都会被问:说一下AQS原理。面对并发编程基础和面试经验,专栏采用通俗简洁无废话无八股文方式,已陆续梳理分享了《一文看懂全部锁机制》、《JUC包之CAS原理》、《volatile核心原理》、《synchronized全能王的原理》,希望可以帮到大家巩固相关核心技术原理。今天我们聊聊AQS....
|
4月前
|
小程序 Java 开发工具
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
本文通过一个生动的例子,探讨了Java中加锁仍可能出现超卖问题的原因及解决方案。作者“JavaDog程序狗”通过模拟空调租赁场景,详细解析了超卖现象及其背后的多线程并发问题。文章介绍了四种解决超卖的方法:乐观锁、悲观锁、分布式锁以及代码级锁,并重点讨论了ReentrantLock的使用。此外,还分析了事务套锁失效的原因及解决办法,强调了事务边界的重要性。
124 2
【Java】@Transactional事务套着ReentrantLock锁,锁竟然失效超卖了
|
4月前
|
Java 开发者
Java多线程教程:使用ReentrantLock实现高级锁功能
Java多线程教程:使用ReentrantLock实现高级锁功能
48 1
|
4月前
|
Java
Java 并发编程:理解并应用 ReentrantLock
【7月更文挑战第56天】 在多线程环境下,为了保证数据一致性和程序正确性,我们需要对共享资源进行同步访问。Java提供了多种并发工具来帮助我们实现这一目标,其中ReentrantLock是一个功能强大且灵活的同步机制。本文将深入探讨ReentrantLock的基本原理、使用方法以及与synchronized关键字的区别,帮助读者更好地理解和应用这一重要的并发编程工具。
|
3月前
|
Java
JAVA并发编程ReentrantLock核心原理剖析
本文介绍了Java并发编程中ReentrantLock的重要性和优势,详细解析了其原理及源码实现。ReentrantLock作为一种可重入锁,弥补了synchronized的不足,如支持公平锁与非公平锁、响应中断等。文章通过源码分析,展示了ReentrantLock如何基于AQS实现公平锁和非公平锁,并解释了两者的具体实现过程。
|
4月前
|
传感器 C# 监控
硬件交互新体验:WPF与传感器的完美结合——从初始化串行端口到读取温度数据,一步步教你打造实时监控的智能应用
【8月更文挑战第31天】本文通过详细教程,指导Windows Presentation Foundation (WPF) 开发者如何读取并处理温度传感器数据,增强应用程序的功能性和用户体验。首先,通过`.NET Framework`的`Serial Port`类实现与传感器的串行通信;接着,创建WPF界面显示实时数据;最后,提供示例代码说明如何初始化串行端口及读取数据。无论哪种传感器,只要支持串行通信,均可采用类似方法集成到WPF应用中。适合希望掌握硬件交互技术的WPF开发者参考。
82 0
|
4月前
|
开发者 C# 存储
WPF开发者必读:资源字典应用秘籍,轻松实现样式与模板共享,让你的WPF应用更上一层楼!
【8月更文挑战第31天】在WPF开发中,资源字典是一种强大的工具,用于共享样式、模板、图像等资源,提高了应用的可维护性和可扩展性。本文介绍了资源字典的基础知识、创建方法及最佳实践,并通过示例展示了如何在项目中有效利用资源字典,实现资源的重用和动态绑定。
114 0
|
4月前
|
安全 Java
Java并发编程实战:使用synchronized和ReentrantLock实现线程安全
【8月更文挑战第31天】在Java并发编程中,保证线程安全是至关重要的。本文将通过对比synchronized和ReentrantLock两种锁机制,深入探讨它们在实现线程安全方面的优缺点,并通过代码示例展示如何使用这两种锁来保护共享资源。
|
4月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
49 0
|
Java Linux
Java AQS 实现——Condition
本文着重介绍 AQS 的 Condition 实现方式。