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

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

2. 从ReentrantLock到AQS

没看过AQS,ReentrantLock 总该了解点吧,有道是知兄莫如弟,那么我们就由其入手,旁敲侧击。

简述

描述一下ReentrantLock的背景:

我们都知道 synchronized 关键字是用于加锁,但是这种锁对于性能影响比较大,因为线程在获取资源时必须处于等待状态,没有额外的尝试机制。所以在jdk1.5 的时候,java 提供了 ReentrantLock ,用于替代 synchronized.

ReentrantLoack 具有可重入,可中断,可限时,公平锁非公平锁等特点。

ReentrantLock 的简单使用:

val lock = ReentrantLock()
lock.lock() //加锁
//业务逻辑
lock.unlock()  //释放 

与AQS的关系

看到这,你可能会说,你说了那么多,那它到底和 AQS 有啥关系?请看下图所示:

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

ReentrantLock 内部有一个抽象内部类 Sync 继承了 AbstractQueuedSynchronizer ,默认构造函数中又实例化了 NonfairSync 类 (Sync 的子类)。对于外部而言,只关注 lock 与 unlock 方法,但实际上内部都是调用了 AbstractQueuedSynchronizer 的方法。

流程剖析

看了上面的图,只是为加深一个印象,那就是 ReentrantLock 中用到了 AQS ,接下来我们通过下面这个简单流程解析,来看一下 AQS 在 ReentrantLock 的运用以及其原理。

为了便于理解,我们整个流程都是以 NonfairSync 即不公平锁的伪源码为例(公平非公平差距并不大)。

lock()

从加锁方法开始,如下:

class NonfairSync .. -> 
   fun lock() -> {
      👉 1. if (compareAndSetState(0, 1))
            👉 2. setExclusiveOwnerThread(Thread.currentThread())
            else
               👇 acquire(1) //独占模式获取 
       fun acquire(arg:Int=1) -> {
          //尝试获取 && 将当前线程添加到等待队列中,并通过CAS的方式不断尝试获取前一个节点
         👉 3.  if (!tryAcquire(arg) &&
             👉 4.  acquireQueued(addWaiter(Node.EXCLUSIVE), arg) )
1.compareAndSetState

这个方法的意思是尝试获取锁,其内部的操作如下:如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。

也就是说,当前我们预估值为0,即我们预估当前没有线程占用资源,如果操作时,发现 这个要实际操作的值真的是0,也就是当前资源并没有其他线程占用,那么我们就将其更新为1,表示当前资源已经被占用。

而 AQS 内部正是有一个 int 型变量 state ,其作用正是代表当前加锁状态。

private volatile int state;

当线程尝试获取锁成功后,如果同一个线程再次尝试获取锁呢?我们称之为锁的重入,那怎么做呢?总不能我自己再获取一把锁?不可能吧,对于一个资源,怎么可能生成两把锁被同一个线程占用。离谱!那怎么办呢?

这时候就轮到 setExclusiveOwnerThread 方法了,我们看看它的实现。

2.setExclusiveOwnerThread
protected final void setExclusiveOwnerThread(Thread thread) - {
        exclusiveOwnerThread = thread

内部是设置了当前的线程对象,而这个 exclusiveOwnerThread 正是 AQS 另一个变量,代表了 当前拥有锁的线程 。这个在哪里用呢,我们看下面方法。

3.tryAcquire

这个方法的含义是以不公平的方式去获取锁,其伪代码如下:

 fun tryAcquire(acquires:Int=1) -> {
    👇
     fun nonfairTryAcquire(acquires):Boolean -> {
          val current = 当前线程对象
          val c = getState()
     1. 👉 if(c== 0 && compareAndSetState(0, acquires)) 
             setExclusiveOwnerThread(current)
             return true
     2. 👉 else if (current= AQS中当前占用资源的线程对象)
             AQS中持有的state += acquires
             return true
           return false

当调用 nonfairTryAcquire 获取锁时,内部的操作很简单:首先获取当前的线程对象与 当前 AQS中储存的 state 状态值,

1.如果当前state=0 并且 通过 compareAndSetState 方法尝试修改 state 成功 则代表当前资源没有线程占用,然后就设置当前拥有锁的线程为当前自己。

2.如果 当前占用资源的线程是自己,那么对 AQS 中的 state+1 ,然后返回true,即代表当前线程获取锁成功。

如果 return false,则代表当前线程获取锁失败。

为什么这里当获取锁的时候是同一个线程就要 state+1 呢?

我们都知道,使用 ReentrantLock 时,我们释放锁调用的是 unLock ,那么我们的切入点就在这了。

4.acquireQueued

这个方法是以死循环的方式不断获取锁,内部代码如下:

 fun acquireQueued(node:Node, arg:Int=1):Boolean  -> {
    //当前是否成功拿到资源
    var failed = true
    try {
        //是否在等待过程中被中断过
        val interrupted = false
        //自旋开始,要么获取锁,
        while (true) {
            val p = 前一个node节点
            //如果前一个节点是head节点并且修改state成功,则表明当前线程已获得锁
            if (p == head && tryAcquire(arg)) {
                //设置新的头结点
                setHead(node)
                //将之前的头结点置null,便于GC回收
                p.next = null // help GC
                failed = false
                return interrupted
            }
            //获取锁失败时调用
            //如果通过前驱结点判断发现当前线程被阻塞并且当前线程已经被中断,则修改 interrupted 标记
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true
        }
    } finally {
        if (failed)
            cancelAcquire(node)
    }
}
总结

当我们调用 lock() 方式加锁时(以不公平锁为例),内部先是以原子方式去尝试修改 AQS 中持有的 state 变量:

 1.如果修改成功,则代表当前资源无线程占用,则获取锁成功,并且将当前 AQS 中的 exclusiveOwnerThread 更新为当前线程对象,以便于后期锁重入时直接返回获取锁成功。

 2.如果最开始修改失败,则调用 acquire 方法去获取锁。 其方法会内部 尝试获取锁一次 ,并且将当前线程添加到等待队列中,然后通过CAS的方式不断自旋,一直获取 父node 节点, 如果 父node 节点是 head头节点 ,就说明当前节点在 队列首部 ,就尝试获取锁,如果获取成功,则更新队列头节点为当前node,并移除当前遍历的父节点。如果获取锁失败,则通过前驱节点判断当前线程是否被阻塞,如果当前线程已经被中断,则更新标记位,并且暂停此循环,等待唤醒。

看完了 lock() 方法的简单分析,是不是觉得感觉自己上错了车,上面只是简单做了一个流程分析,如果细追下去,其中的细节还很深,可能就不是本文所能全部概述,我们只需要知道大体流程即可。

unLock()

   1.👇
   fun unLock() -> { 
       2.👇
       fun release(arg:Int=1):Boolean -> {
                  // 如果 -tryRelease- 结果为true,则唤醒正在等待的队列,即让其他线程获取锁。  
             👉   if (tryRelease(arg))
                    ...
                   unparkSuccessor(h)
           3.👇
           fun tryRelease(arg=1):Boolean -> {
                c = AQS中state变量 - arg
                ..
                if (c == 0) { 
                    //设置AQS中持有的线程为null
                    setExclusiveOwnerThread(null)  
                    return true 
                }
                ..
                setState(c)
                ..       

如上所述伪代码, **unLock ** 方法调用顺序如下,在调用 unLock 方法进行释放锁时,内部其实调用了 relase 方法,其内部又调用了 tryRelease 方法,其内部先是使用 AQS 中的 state 变量-arg(1) ,如果当 c=0 ,则表明当前已经没有线程占用资源,则去唤醒正在等待中的队列,也就是让其他线程开始获取锁。

串一遍思路(非公平锁)

当我们调用 lock 方法时,先是尝试以原子的方式去修改 AQS 内部的state变量值,如果当前 state 值与预期值一致,则更新 AQS 内部state 的变量值为 1 ,并将当前线程对象的引用赋值给 AQS 。

如果在尝试修改 state 变量值的时候失败了,则调用 acquire(xx) 去获取锁,在方法内部将自己添加到当前等待队列中,并且以 CAS 的操作不断自旋,不断尝试去获取当 父node节点 的前一个节点是否等于 head节点 ,并且当前线程是否已经尝试拿到锁,如果前一个节点等于 head节点 并且当前修改 state 变量成功,则代表当前线程已经拿到锁,则将 当前node 节点置为头结点,并移除其前一个节点。当然,AQS 对这个做了很多处理,它并不会一直重复上述重试操作,当经历一段自旋后,它就会以线程中断的方式停止下来,并且取消当前的尝试。

通过理一遍 ReentrantLock 的源码,我们大致了解了一下整个流程,及相应方法的具体职责,这对我们理解 AQS 将起到一些重要的作用。以及自定义一个 自己的重入锁 也将会有帮助。

3. 用AQS写一个重入锁

锁的可重入

指的是当某个线程调用某个方法或者对象获取了一把锁时,再次调用了指定方法,导致的锁的重入。即本身已经获取到了锁,又一次经历了锁的获取,一般情况下,我们会在再次进入时判断当前线程是否获取了锁,如果获取了,就修改同步状态,即 AQS 中的 state+1 。为什么要state+1 ,因为释放锁的时候需要-1啊。

具体代码如下:

4. AQS于我们的日常

说实话,不会使用 AQS ,并不会影响开发任何,在Android开发的现在,各种线程相关的工具库,Rx , 协程 ,都是在降低开发难度,但作为基础,我们还是应该明白有些底层的设计思想,当你或许有一天想要自己去定义一个特定规则的线程工具时,这些看上去好像对我们实际用处不大的东西就都会派上用场。

任何东西的学习,都免不了一个 为什么 ?

比如为什么加了 synchronized 就可以加锁,为什么 ReentrantLock 是可重入呢,当你想要搞清楚这些原因的时候,这些看起来晦涩的东西就是唯一入口。

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