从AQS切入谈一谈不逊色于Synchronized的ReentrantLock可重入锁

26 篇文章 6 订阅
8 篇文章 0 订阅

从AQS切入谈一谈不逊色于Synchronized的ReentrantLock可重入锁

之前在《结合JVM深入谈一谈Synchronized关键字的神奇之处》这篇文章中,提到了Synchronized和ReentrantLock的区别,Synchronized是依赖于JVM实现的,是JVM层面的锁,并且它的优化也是从JVM底层进行优化的。而ReentrantLock可重入锁是API层面的,ReentrantLock这个类实现了Lock这个接口,需要我们使用lock()和unlock()方法手动的进行加锁和释放锁。这篇文章我们从源码入手,来瞅瞅ReentrantLock可重入锁是如何实现的。

首先我们要明确ReentrantLock是一个类,实现了Lock接口,从了实现了最基本的加锁和释放锁功能。
在这里插入图片描述
但是,我们在讲ReentrantLock之前,先要讲一下AQS(AbstractQueuedSynchronizer)——抽象队列同步器,为什么呢?因为实际上ReentrantLock底层就是依赖于AQS实现的。那这个AQS又是什么东西呢?

AQS是用来构建锁或者其它同步器组件,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量来表示持有锁的状态。

抢到资源的线程直接使用资源处理业务逻辑,而那些抢不到资源的线程必然需要一种排队等候机制。抢占资源失败的线程继续去等待,但等候线程仍然保留获取锁的可能且获取锁流程仍在继续

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的节点Node,通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的控制效果
在这里插入图片描述
上面这段话是不是和AQS名字一样抽象?注意字眼【构建】,什么意思?说明AQS是实现其它锁或者和锁类似具有同步功能组件的**基石!是基础!是规范!是框架!**你可以类比于官方发布的JVM规范手册,手册只规定了JVM必须具有哪些内存区域,像是什么堆、栈、方法区等等,但是JVM可不止只有Hotspot这一款落地产品,还有好多其他类型的JVM,它们都是严格依据JVM规范手册实现的,但至于方法区该如何实现?或者说GC垃圾回收该如何实现?那就大同小异了,各有各的实现方式。再比如,建一栋房子,标准是什么?必须有大门,有窗户,有卧室等等,这样盖起来的房子才是合格的,才是完整的,至于你大门具体是什么样子的,大铁门?木质门?窗户是方形的还是圆形的?统统不care。现在多少有些明白AQS是什么了吧,**它相当于构建一个锁需要遵守的标准、规范,约定了一个锁应该具有最基本的获取锁和释放锁等等一些功能。**至于你想构建出来的这个锁什么时候去获取锁,什么时候去释放锁,或者怎样去获取、释放锁,那就由你自己说了算了。

我们说AQS是一个同步框架,所以有很多基于AQS框架的落地实现类,而ReentrantLock可重入锁就是其中之一。那锁和AQS同步器有什么关系呢?实际上,锁是面向锁的使用者的,定义了业务开发者和锁之间交互使用的API,隐藏了锁的具体实现细节,在实际的逻辑业务中想要使用锁的话直接调用API即可。而同步器是面向锁的开发者,每一个锁的具体实现使用统一的同步状态管理、阻塞线程排队通知、唤醒机制等规范。一句话就是,同步器是用来造锁的,而锁是造出来之后拿来用的。
在这里插入图片描述

Synchronized锁和ReentrantLock都是可重入锁,“可重入锁” 指的是同一线程可以多次同一把锁。但是相比synchronized,ReentrantLock增加了一些其他高级功能

  • 等待可中断

    ReentrantLock提供了一种能够中断等待锁的线程机制,调用lock.lockInterruptibly()方法可以让正在等待的线程放弃等待,去做其他事情。

  • 可实现公平锁

    我们要知道,Synchronized锁是非公平锁,也就是说一个线程抢到了锁,在释放锁之后,如果需要的话它还可以继续争抢锁,如果一个线程恰好每次都获取锁成功,那么就会导致线程迟迟得不到执行。而ReentrantLock可以实现公平锁,所谓公平锁,就是说不可能让一个线程一直抢锁成功,而是先等待的线程先获得锁,实现公平轮流加锁。ReentrantLock默认使用的是非公平锁,但是在构造方法中可以指定是否使用公平锁。实际上公平锁是比非公平锁多维护了一个队列,先来后到,每次都是从队列中拿出线程去获取锁,不像非公平那样,每次谁抢到算谁的。

  • 可实现选择性通知

    Synchronized锁有对应的wait()、notify()方法实现线程之间的等待通知机制,同样的,ReentrantLock也应该有一套对应的等待通知机制,await()和signal()方法。这一对方法依赖于Condition接口和newCondition()方法,Condition对象是由Lock对象调用newCondition()方法创建出来的,它有很好的灵活性,在一个Lock对象中可以创建多个Condition对象,线程对象可以注册在指定的Condition中,从而可以有选择性的通知指定的线程。而Synchronized关键字的notify()、notifyAll()方法通知线程时,被通知的线程是由JVM选择的,要么通知一个,要么全部通知,不能像ReentrantLock那样可选择的通知一部分线程。

那么接下来我们从AQS的源码入手,来看看AQS是怎么实现的,弄懂了AQS源码,你也就懂了ReentrantLock可重入锁是怎么实现的。

首先我们来看看AQS源码

  • AQS使用一个volatile修饰的int类型的成员变量来表示同步状态,并通过CAS完成对State状态值的修改
    在这里插入图片描述
    在这里插入图片描述

  • 通过内置的FIFO队列来完成资源获取线程的排队工作,将每个要去抢占资源的线程封装成一个Node节点来实现锁的分配,队列是双向队列。
    在这里插入图片描述

  • 内部类Node数据结构的组成

    • waitStatus用来表示队列中阻塞线程的等待状态,队列中每一个Node都是一个线程
    • prev指向队列中前一个节点
    • next指向队列中后一个节点
    • thread表示当前线程
      在这里插入图片描述
  • AQS同步队列的基本结构
    在这里插入图片描述

然后再看看ReentrantLock的源码

  • 首先我们来看一看ReentrantLock和AQS的基本架构
    在这里插入图片描述

    实际上,ReentrantLock是Lock接口的实现类,基本上都是通过聚合了一个队列同步器的子类完成线程访问控制的。ReentrantLock中有一个继承了AQS的内部类Sync,公平锁和非公平锁的又都继承了这个子类,所以公平锁和非公平锁的实现了都依赖于这个AQS的子类Sync。
    在这里插入图片描述
    包括所有锁的方法,底层也都是调用Sync相对应的方法
    在这里插入图片描述

  • 公平锁和非公平锁

    公平锁:先请求获取锁的线程先得到锁,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中。

    非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁
    在这里插入图片描述

    我们之前说了Synchronized是非公平锁,而ReentrantLock不仅实现了非公平锁,还实现了公平锁。公平锁和非公平锁的lock()方法唯一区别在于,公平锁在尝试获取锁的时候,还会判断线程阻塞等待队列中有没有正在排队等候的有效线程节点。
    在这里插入图片描述

    **hasQueuedPredecessors()方法会判断下一个有效线程节点是否还是当前线程,**进而满足了公平锁,防止一个线程一直占用锁
    在这里插入图片描述

    ReentrantLock默认是非公平锁,我们以一个具体的银行办理业务为例,来分析一下非公平锁的源码
    在这里插入图片描述

    • lock()方法——加锁

      1. 首先调用lock()方法加锁,实际上是调用的内部类Sync的lock()方法,相当于尝试获取受理业务窗口。加锁是利用CAS自旋尝试获取锁,如果获取成功,state状态设置为1,并把当前锁占用线程设置为自己。
        在这里插入图片描述
        此时,如果第二个线程也尝试获取锁,由于第一个线程已经占用了锁,并且把state状态由0变成了1,表示当前锁已被占用,所以第二个线程暂时无法获取锁
        在这里插入图片描述

      2. acquire()方法分为3部分
        在这里插入图片描述

        img

      3. 首先调用tryAcquire()方法尝试获取锁

        tryAcquire()方法是父类AQS的方法,留给子类具体实现
        在这里插入图片描述

        nonfairTryAcquire()方法非公平锁尝试获取锁
        在这里插入图片描述

      4. 如果第二个线程尝试获取锁没有成功,就会继续调用addWaiter()方法将当前线程封装成Node节点,准备入队列
        在这里插入图片描述

        由于第二个线程时第一个进入队列的Node,此时队列还为空,所以调用enq()方法入列
        在这里插入图片描述

        如果当前队列为空,还没有进行过初始化,enq()方法会创建一个哨兵节点,进行必要的初始化。双向链表中,第一个节点为虚节点,也叫哨兵节点,并不存储任何信息,只是占位作用,真正的第一个有数据的节点,是从第二个节点开始的
        在这里插入图片描述

        初始化之后然后在队列尾部插入当前需要排队的线程
        在这里插入图片描述

        当第三个线程如果也没有获取到锁,调用addWaiter()方法加入等待队列时,由于队列已经不为空了,所以不会调用enq()方法,直接在尾部插入这个节点。
        在这里插入图片描述

      5. 线程节点加入队列之后,就会调用acquireQueued()方法,队列中的线程也在不断尝试获取锁
        在这里插入图片描述

        acquireQueued()方法如果再次尝试获取锁失败,就会调用shouldParkAterFailedAcquire()方法和parkAndCheckInterrupt()方法
        在这里插入图片描述

        shouldParkAterFailedAcquire()方法是线程尝试获取锁失败之后,主要作用是将waitStatus改为-1,说明应该阻塞

        在这里插入图片描述

        parkAndCheckInterrupt()方法调用LockSupport.park()方法,将当前线程真正的挂起

        在这里插入图片描述

        此时,所有排队的线程都挂起,acquireQueued()方法被卡住,并没有执行完
        在这里插入图片描述

    • unlock()方法——解锁

      1. unlock()方法底层调用的是sync的release()方法,会进一步调用tryRelease()方法尝试释放锁
        在这里插入图片描述

      2. tryRelease()方法尝试释放锁
        在这里插入图片描述

      3. 尝试释放锁返回true,就会进一步调用unparkSuccessor()方法唤醒阻塞的线程
        在这里插入图片描述
        在这里插入图片描述 4. 得到permit许可证的线程,就可以被唤醒继续执行,此时一直在循环的acquireQueued()的方法得以继续执行

        在这里插入图片描述 在这里插入图片描述

通过上面对ReentrantLock的加锁和解锁流程的解读,我们可以看到,实际上ReentrantLock锁的基本功能都是依赖于内部类Sync实现的,底层都是直接调用Sync的方法。而这个Sync是继承了AQS的子类,由此我们了解到了AQS框架的作用。

当然,AQS是一个同步框架,基于AQS实现的锁或者其它同步组件肯定不只是ReentrantLock,那么接下来我们就来讲一讲**基于AQS的同步组件CountDownLatch、CyclicBarrier、Semaphore。**它们都是并发同步工具类。

  • CountDownLatch

    用来协调多个线程之间的同步,通常用来控制线程等待,它可以让某一个线程等待其它线程完成操作之后,再继续执行。

    CountDownLatch类主要有两个方法:

    • countDown()方法会使得计数器减1
    • await()方法会阻塞当前线程,直到计数器变成0

    这里你有可能想到了join()方法,join方法也是用于让当前执行线程等到join线程执行结束,其原理是不停检查join线程是否存活,如果join线程存活则让当前线程永久等待。直到join线程中止后,线程的notifyAll方法会被调用,调用notifyAll方法是在JVM实现的。而CountDownLatch相比于join方法,采用了计数器的方法,只要是计数器减少到0,当前线程就可以继续往下执行,因此又称为倒计时器。更加灵活,更适合分阶段业务的处理,例如多线程读取多个文件处理的场景。
    在这里插入图片描述

  • CyclicBarrier

    从字面意思上来看,CyclicBarrier是一个可循环使用的屏障。它的作用是让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。从作用上CyclicBarrier和CountDownLatch是非常相似的,不过CyclicBarrier比CountDownLatch更加复杂和强大。相比于CountDownLatch,CountDownLatch的计数器是逐个减1,并且计数器只能使用一次。而CyclicBarrier的计数器是逐个加1,并且可以使用reset()方法重置。CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
    在这里插入图片描述

  • Semaphore

    用于多个共享资源的互斥使用,以及控制并发线程数协调各个线程,又称为信号量。synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

    主要有三个方法:

    • acquire()方法获取一个许可证
    • release()方法归还许可证
    • tryAcquire()方法尝试获取许可证
      在这里插入图片描述

至此,这篇文章我们讲了ReentrantLock可重入锁,讲了它的底层实现AQS框架,以及基于AQS框架实现的其它同步组件。与并发相关的同步机制,我们已经讲了Synchronize同步关键字、ReentrantLock可重入锁,三板斧中还剩下一个volatile这个轻量级的同步机制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
重入锁(ReentrantLock)是一种独占锁,也就是说同一时间只能有一个线程持有该锁。与 synchronized 关键字不同的是,重入锁可以支持公平锁和非公平锁两种模式,而 synchronized 关键字只支持非公平锁。 重入锁的实现原理是基于 AQS(AbstractQueuedSynchronizer)框架,利用了 CAS(Compare And Swap)操作和 volatile 关键字。 重入锁的核心思想是“可重入性”,也就是说如果当前线程已经持有了该锁,那么它可以重复地获取该锁而不会被阻塞。在重入锁内部,使用了一个计数器来记录当前线程持有该锁的次数。每当该线程获取一次锁时,计数器就加 1,释放一次锁时,计数器就减 1,只有当计数器为 0 时,其他线程才有机会获取该锁。 重入锁的基本使用方法如下: ```java import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTest { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " get lock"); Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + " release lock"); } }, "Thread-1").start(); new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " get lock"); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + " release lock"); } }, "Thread-2").start(); } } ``` 在上面的示例代码中,我们创建了两个线程,分别尝试获取重入锁。由于重入锁支持可重入性,因此第二个线程可以成功地获取到该锁,而不会被阻塞。当第一个线程释放锁后,第二个线程才会获取到锁并执行相应的操作。 需要注意的是,使用重入锁时一定要记得在 finally 块中释放锁,否则可能会导致死锁的问题。同时,在获取锁时也可以设置超时时间,避免由于获取锁失败而导致的线程阻塞问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值