【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码

这篇具有很好参考价值的文章主要介绍了【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

前言:

前置知识: 

什么是公平锁与非公平锁?

尝试自己构造一把锁:

ReentrantLock源码:

加锁:

解锁:

总结:


 文章来源地址https://www.toymoban.com/news/detail-807641.html

前言:

在并发编程中,线程安全是一个重要的问题。为了保证多个线程之间的互斥访问和正确的同步操作,Java提供了一种强大的锁机制——ReentrantLock(可重入锁)。

与synchronized相比,ReentrantLock提供了更加灵活和强大的功能。它支持公平锁和非公平锁两种模式,可以通过lock()和unlock()方法手动控制锁的获取和释放,并且可以实现可重入特性,即同一个线程可以多次获得同一个锁而不会发生死锁。

在本文中,我们将详细讲解ReentrantLock的使用方法,包括基本的锁操作、可重入特性、公平性设置、条件变量和中断等。我们还将对比ReentrantLock和synchronized的性能差异,并给出一些适用场景的建议。

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

在正式介绍ReentrantLock之前,我们要先来学习一些前置知识,方便大家更好的理解ReentrantLock源码。

前置知识: 

什么是公平锁与非公平锁?

公平锁:

        公平锁是指多个线程按照申请锁的顺序依次获取锁,即先到先得的原则。当一个线程释放锁后,等待时间最长的那个线程将获得锁的访问权。公平锁可以避免线程饥饿,保证每个线程都有机会获得锁,但可能会降低系统的整体吞吐量。

非公平锁:

        非公平锁是指多个线程竞争锁时,不考虑线程的等待时间,直接尝试获取锁。如果当前锁没有被其他线程持有,则该线程立即获得锁的访问权,即先到先得。非公平锁的性能相对较好,因为它减少了线程切换和调度的开销,但可能会导致某些线程一直无法获取到锁,产生线程饥饿的问题。

简而言之:公平锁在线程获取锁的时候,等待时间最长的线程获取锁的访问权。非公平锁是线程自由竞争,不存在按照等待时间排序。

而ReentrantLock既可以是公平锁,也可以是非公平锁。无参构造情况下是非公平锁

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

尝试自己构造一把锁:

锁的基本原理就是:加锁就是放行或者阻塞当前线程。放锁就是唤醒被阻塞的线程。

基于这个思想,我们很快就可以设计出一个最简单的锁:

public class LiLocks {
    int statue =0;
    public void lock()
    {
        while ( statue!=0)
        {
            System.out.println(Thread.currentThread().getName()+"等待锁");
        }
        statue=1;
    }
    public void unlock()
    {
        statue=0;
    }
}

但是这个锁存在一个最基本的问题:并不能处理全部情况

        理想状态下,A线程进入lock之后,先判断当前statue==0,之后做statue自增操作。这样当下一个线程进入的时候就会被阻塞到while循环中,直到A线程进入unlock方法对statue做递减操作。如果statue==0的话,就相当于释放锁。

而在有些特殊情况下,当A线程比较完之后,还没来得及给statue赋值,另外一个B线程就抢到了资源调度,由于此时statue还没变值,所以此时B线程仍然可以通过比较不进入while循环。就造成了加锁失败的情况

 也就是说:如果我们想要优化这个锁,那么就需要使得判断statue==0和赋值statue=1具有原子性。

这两步就是CAS(Compare And Swap),只不过我们的CAS是自己写的,所以不具备原子性。那么我们调用Java为我们提供的具有原子性的CAS操作就好了。

而Java为我们提供的具有原子性的cas操作来自Unsafe类,那么我们使用Unsafe类来改造代码:

public class LiLock {
    private static Unsafe unsafe;
    volatile  int statue =0;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    long objectOffset = unsafe.objectFieldOffset(LiLock.class.getDeclaredField("statue"));

    public LiLock() throws NoSuchFieldException {

    }

    public void lock() throws NoSuchFieldException {

        while (!unsafe.compareAndSwapInt(this, objectOffset, 0, 1))
        {
            System.out.println(Thread.currentThread().getName()+" is waiting");
        }
    }
    public void unlock() {
        statue=0;
    }
}

在这里,我们简单实现了一个自旋锁。而我们如果想要把这个锁改为公平锁,要怎么做呢?

也就是说:当此时有1,2,3 总共三个线程的时候,如果1线程获取到了锁,那么我们如何让2,3线程按照顺序获取锁呢?

假设线程2此时已经被while循环阻塞了,如果此时线程3来的话,那么我们是不能让线程3进入while阻塞的。如果3也被阻塞的话,那么不就是说2和3又在同一起跑线了。无法保证2先来就先获取到锁。

所以此时的问题就在于:如何不使用相同的while来阻塞3线程,让2线程优先获得到锁?

我们回顾一下我们之前学过的阻塞一个线程的方式:

1.wait():使用不了,他需要和synchronized 关键字一起使用,而我们是在自己写锁,当然不能使用synchronized

2.sleep():使用不了,我们不知道线程1需要多长时间才会释放锁,因此无法确定让线程2,3睡眠多长时间。

3.while(ture):使用不了,使用这个还是将线程2,3放到一个起跑线了,无法确保先来的线程2一定能过比线程3获取到锁

基于这三种方法都使用不了,我们只能采用第四种:LockSupport类中的park()方法,它可以阻塞一个线程,使其不消耗cpu资源,直至被其他线程唤醒或者中断

那么我们直接对线程3采用park方法就可以了。通过这种机制,我们就实现了公平锁的机制。

通过上述知识,我们引入了公平锁的基本实现方式,理解如何构造一个公平锁有利于我们阅读ReentrantLock源码。

ReentrantLock源码:

加锁:

        最外层:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

这段代码的基本逻辑为:如果当前线程不能获取锁,则会进入等待队列中等待,并在等待过程中不断尝试获取锁。如果当前线程被中断,则会重新设置中断标志。

我们在这里主要聊的是加锁,因此我们就先看tryAcquire这个方法:

这个方法分为两个实现:公平锁非公平锁,因此我们逐个来看:

公平锁:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

我们来先看一下这个方法中调用的一些比较重要的方法:

1.setExclusiveOwnerThread():将锁的拥有线程修改为当前线程。

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

2.compareAndSetState():具有原子性的CAS操作。

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

3.getExclusiveOwnerThread():获取当前持有锁的线程对象。

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

4.setState:设置当前锁的状态。 

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

5.hasQueuedPredecessors():判断当前线程是否需要排队

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

在了解完这些方法之后,其实了解公平锁的代码逻辑就很简单了。

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

公平锁的基本代码逻辑为:

1.获取当前线程和锁状态。

2.如果当前锁没有被线程获取(c==0),进一步判断:当前线程是否需要排队(!hasQueuedPredecessors()),如果不需要就进行CAS操作,将状态从0变为acquirs。并且设置锁的持有线程为当前线程。返回结果为true。

3.如果当前锁已经被线程获取,则再次判断获取锁的线程是不是当前线程(current == getExclusiveOwnerThread())如果是的话,更新状态值nextc为c+acquires(setStates(sextc)),返回ture。

4.如果当前锁已经被获取,且获取线程不是当前线程,则返回false

这里的整体逻辑其实和我们自己构造的公平锁逻辑很相似了。主要的区别在于:我们为什么在这里把获取锁的状态值设置为了acquire,而不是简单的1?

其实道理很简单,设置为acquire之后,我们就可以根据状态值判断锁的重入次数。这样在释放的时候才不会释放错误。        


非公平锁:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

非公平锁的代码逻辑几乎和公平锁一样,只不过在判断当前锁没有线程获取的时候,我们不再调用 hasQueuedPredecessors()方法来检测当前线程是否需要排队。也就是自由竞争。

让我们回到加锁的最外层: 

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

如何把一个线程加入到等待队列中也是热门面试题,因此我们在这里也顺带讲一下:

在ReentrantLock的加锁外层中,为了把一个线程放到等待队列中,我们使用了addWaiter方法:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

addWaiter是AQS提供的代码:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

整体的代码逻辑也很简单:

先接收传递过来的节点,再用一个for构成的死循环抱住核心逻辑代码。这样是确保进入该方法的线程都可以被添加到队列当中。因为for死循环了。

让我们解读一下源代码:

1.先获取到同步队列的尾节点,之后对尾节点进行非空判断,如果为空,就先初始化队列。

2.如果尾节点不为空,先设置待加入节点的前驱节点为尾节点。

3.使用cas操作,使得tail更新为node。(tail为始终指向尾节点的一个变量

4.将实际尾节点的next指针指向node。

5.返回node。

这样我们就完成了一次添加尾节点。其实就是一个很简单的尾插法。而我们要讲一下:为什么在更新Tail变量的时候,要使用cas操作?

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

这是因为在多线程竞争的条件下,我们要保证始终只有一个节点获取到tail,成为真正的尾节点,如果不使用cas算法来对tail进行维护的话,那么多个线程同时获取到tail之后,就会引发线程竞争

目前我们已经知道了addwaiter的作用就是把线程加入到阻塞队列中,那么下一步很明显就应该是阻塞在队列中的线程了,但是我们想一想逻辑:阻塞队列是先进先出的,那么在阻塞和唤醒的时候,就应该是:当前线程自己阻塞后,由前一个线程唤醒。由图可看:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

而我们的新加节点也不是直接阻塞的,他需要告知前一个节点,而node节点 中就为我们维护了这样一个变量:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

某一个node对象的waitStatus的值为0的时候,意味着他在释放锁之后不需要unpark其他对象,反之如果它的值为-1的话,就需要在释放锁之后unpark其他对象。 

那么很显然,这段代码逻辑应该就是在acquireQueued这个方法中执行的;

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

 让我们来看一看这个方法:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

这段方法的逻辑为:先获取到这个节点的前一个节点:如果前一个节点为头节点的话,说明当前线程是一个进行排队的,那么就调用tryAcquire方法进行加锁。

如果加锁成功,就调用setHead方法,把线程变为头节点。

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

这种方式使得加锁成功后,线程不会再留到这个队列当中。

如果不是头节点,就调用shouldParkAfterFailedAcquire()方法:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

这个其实就是我们之前讲到的设置waitStatus的逻辑。 

我们分别来看这几个代码的逻辑:

1.如果前驱节点的waitStatus等于-1。就返回ture。

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

2.如果前驱节点的waitStatus>0.则说明此前驱节点不可用,我们就一直往前找,直到找到前驱节点小于等于0的节点。 

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

3.如果前驱节点的waitStatus=0,就使用CAS操作,将其值更新为-1。【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

我们来看看外围方法:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言 也就是说:当shouldParkAfterFailedAcquire方法返回ture(前驱节点的waitstatus=-1)的时候,就调用parkandCheckInterrupt方法

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

解锁:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

这是最外层的方法:基本逻辑为:

先调用tryRelease(arg)来尝试释放锁,如果锁释放成功,则肇东当前头节点,如果头节点不为空且waitstatus的值不为0,则证明有节点需要头节点unpark,那么就调用unparkSuccessor

否则tryRelease(arg)返回false,标识锁释放失败。

因此我们来看一看tryRelease方法:

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

整体的逻辑为:先用getstate-release(锁支持重入)。之后判断当前线程是否是持有锁的线程。如果不是就抛出异常。之后创建锁的标志量free,false标识释放失败,true标识释放成功。那么当c==0的时候,意味着锁释放完全了,就更新标识量free为true。并且同步设置锁的拥有者为null。再最外层更新锁的状态量state

这就是解锁的全部过程。

总结:

ReentrantLock 是 Java 并发包中的一个重要类,它提供了一种可重入的互斥锁实现。在多线程环境下,ReentrantLock 提供了更灵活和强大的同步控制机制,相比于 synchronized 关键字,它具有更多的特性和扩展能力。

首先,ReentrantLock 采用了独占模式,即同一时刻只允许一个线程持有锁。这保证了被锁保护的临界区只能被一个线程访问,从而避免了多个线程同时修改共享资源导致的数据竞争和不一致性。

其次,ReentrantLock 支持可重入,也就是说同一个线程可以多次获取同一个锁。这种特性使得线程可以在持有锁的情况下重复进入同一个临界区,并且在退出时释放锁。这样的设计简化了编程模型,并且避免了死锁的可能。

此外,ReentrantLock 还提供了公平性选择的机制。通过构造函数传入的参数可以选择公平模式或非公平模式,默认是非公平模式。在公平模式下,锁会按照请求的顺序分配给等待线程,避免了线程饥饿的情况。在非公平模式下,锁的获取是无序的,可能会导致某些线程一直获取不到锁。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码,【从零开始学习JAVA重要集合】,学习,java,开发语言

 

到了这里,关于【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【从零开始学习JAVA | 第三十篇】方法引用

    目录 前言: 方法引用: 方法引用基本概念: 方法可以被引用的条件: 方法引用的种类: 方法引用的优点: 总结: 方法引用作为一个重要的知识点,虽然他使用起来很复杂,而且会降低代码的可读性,但是如果用好了方法引用,我们也会获得不错的效率,因此我们在今天

    2024年02月15日
    浏览(48)
  • 【从零开始学习JAVA | 第三十八篇】应用多线程

    目录 前言: 多线程的实现方式: Thread常见的成员方法: 总结:            多线程的引入不仅仅是提高计算机处理能力的技术手段,更是适应当前时代对效率和性能要求的必然选择。在本文中,我们将深入探讨多线程的应用和实践,帮助读者更好地理解和应用多线程技术,

    2024年02月13日
    浏览(67)
  • 【从零开始学习JAVA | 第三十一篇】异常体系介绍

            本文我们将为大家介绍一下异常的整个体系,而我们学习异常,不是为了敲代码的时候不出异常,而是为了能够熟练的处理异常,如何解决代码中的异常。  我们就以这张图作为线索来详细介绍一下Java中的异常: 在Java中, Exception(异常)是一种表示非致命错误或异

    2024年02月15日
    浏览(91)
  • 【从零开始学习JAVA | 第三十七篇】初识多线程

    目录 前言: ​编辑 引入: 多线程:         什么是多线程:         多线程的意义:         多线程的应用场景: 总结:                 本章节我们将开始学习多线程,多线程是一个很重要的知识点,他在我们实际开发中应用广泛并且基础,可以说掌握多线程编写程

    2024年02月14日
    浏览(107)
  • 【从零开始学习JAVA | 第三十二篇】 异常(下)新手必学!

    目录 前言:  Exceptions(异常): 异常的两大作用: 异常的处理方式: 1.JVM默认处理  2.自己捕获异常 3.抛出处理 自定义异常: 异常的优点: 总结:         前文我们详细的为大家介绍了整个异常体系的框架,本篇我们将为大家介绍 Exceptions 异常,我们会讲解他的作用以及

    2024年02月15日
    浏览(69)
  • 【从零开始学习JAVA | 第三十五篇】IO流综合练习

    目录 前言: 1.拷贝文件(含子文件) 思路: 2.文件加密 思路: 3.修改文件中的数据: 思路: 总结:         在前面我们为大家介绍了FILE类和IO类。这篇文章我们来练习一些综合使用的例子以此来巩固我们自己的所学知识。 建立一个读文件的流来读取文件,一个写文件的流

    2024年02月14日
    浏览(52)
  • 【从零开始学习 UVM】9.2、UVM Config DB —— UVM config database 详解【重要】

    UVM有一个内部数据库表,可以将值存储在给定名称下,并且稍后可以由其他TestBench组件检索。 uvm_config_db 类提供了一个方便的接口,位于 uvm_resource_db 之上,以简化用于uvm_component实例的基本接口。 请注意,所有函数都是静态的,并且必须使用 :: 作用域运算符调用 。 这样的配

    2023年04月09日
    浏览(79)
  • 【从零开始学习Linux】背景知识与获取环境

     哈喽,哈喽,大家好~ 我是你们的老朋友: 保护小周ღ    本期给大家带来的是 Linux 操作系统的简介,以及如何获取一个Linux 的环境 , 作为Linux 章节起始篇,如果不妥之处,欢迎批评指正~ 本期收录于博主的专栏 : JavaEE_保护小周ღ的博客-CSDN博客 适用于编程初学者,感兴

    2024年02月15日
    浏览(43)
  • 从零开始-学习网络必须掌握的基础知识

    从零开始!学习网络必须掌握的基础知识 完整的100个网络知识,可留言,会发送!点击关注,可获得网工大礼包哈

    2024年02月15日
    浏览(55)
  • C++知识第三篇之继承

    继承是 面向对象编程 的重要特征,是对类设计层次的复用 1.继承定义 Parent :是父类,也称作基类(base class); Student :是子类,也称作派生类(derived class) public :表示继承方式为公共继承 2.继承方式 子类继承父类,有3种继承方式 继承方式的作用是: 对于父类非private的成员

    2024年02月06日
    浏览(71)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包