ReentrantLock介绍及使用(超详细)

这篇具有很好参考价值的文章主要介绍了ReentrantLock介绍及使用(超详细)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

点击 Mr.绵羊的知识星球 解锁更多优质文章。

目录

一、介绍

1. 简介

2. 是什么类型的锁

3. 优点

4. 原理

5. 主要方法

6. 使用时注意事项

二、实际应用

1. 案例一

2. 案例二


一、介绍

1. 简介

    ReentrantLock是一种基于AQS(Abstract Queued Synchronizer)框架的应用实现,是JDK中一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。

2. 是什么类型的锁

    (1) 公平锁或非公平锁(下面案例中有实际代码,自己执行一遍更容易理解)

    a. 公平锁:先来的线程先执行,排成排按顺序。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

    b. 非公平锁:后来的线程有可能先执行,可插队不一定按顺序。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

    (2) 互斥锁

    一次只能执行一个线程。

    (3) 可重入锁

    同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

    比较抽象,解释下:假如景区有3个收费项目,如果每个项目单独玩都需要收费。但是你买了个VIP。进入景区大门的时候工作人员为了方便,直接给你挂了个牌子,里面三个项目的工作人员看到你的牌子,就认为你已经买过该项目的门票,不在向你收费,进去就行。

3. 优点

    (1) 可中断并且可以设置超时时间。

    (2) 可以根据业务场景使用公平锁或非公平锁。

    (3) 获取锁可设置超时。

    (4) 可绑定多个条件(Condition)。

   关于他的优点肯定是和其他锁比较得来的,一般都是和synchronized(synchronized介绍及使用 这篇文章稍后上传)比较。

4. 原理

    ReentrantLock是怎么实现锁同步的呢?咱们先看代码:

reentrantlock使用,Java,java,开发语言

reentrantlock使用,Java,java,开发语言

reentrantlock使用,Java,java,开发语言

reentrantlock使用,Java,java,开发语言

reentrantlock使用,Java,java,开发语言

   点来点去发现,具体的加锁逻辑的实现在AQS这个抽象类中,所以想要了解其实现原理,还得看看AbstractQueuedSynchronizer(AQS详解 这篇文章稍后上传)

5. 主要方法

    getHoldCount():当前线程调用 lock() 方法的次数。

    getQueueLength():当前正在等待获取 Lock 锁的线程的估计数。

    getWaitQueueLength(Condition condition):当前正在等待状态的线程的估计数,需要传入 Condition 对象。

    hasWaiters(Condition condition):查询是否有线程正在等待与 Lock 锁有关的 Condition 条件。

    hasQueuedThread(Thread thread):查询指定的线程是否正在等待获取 Lock 锁。

    hasQueuedThreads():查询是否有线程正在等待获取此锁定。

    isFair():判断当前 Lock 锁是不是公平锁。

    isHeldByCurrentThread():查询当前线程是否保持此锁定。

    isLocked():查询此锁定是否由任意线程保持。

    tryLock():线程尝试获取锁,如果获取成功,则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false。

    tryLock(long timeout,TimeUnit unit):线程如果在指定等待时间内获得了锁,就返回true,否则返回 false。

    lockInterruptibly():如果当前线程未被中断,则获取该锁定,如果已经被中断则出现异常。

    方法详细介绍可以看最下面参考文章:ReentrantLock类中的方法

6. 使用时注意事项

    (1) 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

    说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。

    (2) 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。

    说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。

    (3) 在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。

    a. 在lock方法与try代码块之间的方法调用抛出异常,无法解锁,造成其它线程无法成功获取锁。

    b. 如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。

    c. 在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。

    d. 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。

    说明:Lock对象的unlock方法在执行时,它会调用AQS的tryRelease方法(取决于具体实现类),如果当前线程不持有锁,则抛出IllegalMonitorStateException异常。

上面就是阿里规约中对锁的使用一些注意事项,感兴趣可以学习下!阿里巴巴编码规范学习及应用

二、实际应用

    某个方法或者共享变量不能被多个线程同时操作,可以用ReentrantLock进行加锁。当然这只是其中的一种解决方式。

    注意:使用之前你要知道,使用锁的话执行会相对较慢,因为在加锁的代码块内,每次只能执行一个线程。举个栗子(简单看看就行。git地址):

public static void main(String[] args) {
    // 设置执行次数
    int executeCount = 2;
    // 定义方法开始时间,单位: ms
    long startTime = System.currentTimeMillis();
    // java.util.concurrent提供的API,在该案例中主要是为了让主线程等待子线程结束后进行打印
    CountDownLatch countDownLatch = new CountDownLatch(executeCount);

    ReentrantLock lock = new ReentrantLock();
    for (int index = 0; index < executeCount; index++) {
        new Thread(() -> {
            LOGGER.info("current thread name: {} start.", Thread.currentThread().getName());
            // 加锁
            lock.lock();
            try {
                // 休眠1秒
                sleep(1);
            } finally {
                // 一定要在finally解锁
                lock.unlock();
            }

            /// 休眠1秒,如果测试不加锁耗时可将注释打开并对上面加锁逻辑进行注释
            // sleep(1);
            LOGGER.info("current thread name: {} end.", Thread.currentThread().getName());
            countDownLatch.countDown();
        }).start();
    }

    try {
        // 主线程等待子线程结束后打印log信息
        countDownLatch.await();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

    LOGGER.info("cost time: {}ms", System.currentTimeMillis() - startTime);
}

通过上面的案例可知:加锁耗时2075ms,不加锁耗时1058ms。这有点耗时啊,所以如果必须加锁的话一定要把锁的范围控制在最小,避免浪费太多时间。但是这个加锁方式有个小问题,什么问题呢?我们继续往下看!

1. 案例一

(1) 场景:

    王涛是公司的开发人员,他写了一个远程调用的方法,并且在调用这个方法前加了锁,调用之后解锁。他在本地测试没啥问题,所以就上线了。突然有一天很多客户点击王涛写的方法,结果发现一直加载中...没有任何提示。这线上问题一发生,搞的王涛和经理连夜加班找bug,之后进行如下修改。

(2) 代码:git地址

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLockCase1
 * 使用ReentrantLock调用方法超时处理
 *
 * @author wxy
 * @date 2023-02-16
 */
public class ReentrantLockCase1 {
    private static final Logger LOGGER = LoggerFactory.getLogger(ReentrantLockCase1.class);

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        for (int index = 0; index < 2; index++) {
            new Thread(() -> {
                /*---修改前代码---*/
                /*try {
                    lock.lock();
                    // 调用某个方法,这个方法会超时60s(偶现)
                    timeoutApi();
                } finally {
                    lock.unlock();
                }*/

                /*---修改后代码---*/
                try {
                    // 设置如果线程1正在调用,线程2等待5秒,5秒后你可以对线程2进行处理: 比如返回提示、线程处理...
                    // 如果你不设置超时时间,那么所有的线程就会等待前一个线程解锁,具体怎么等待请看AQS详解
                    // 备注: 正常情况下超时时间应该在配置文件中配置,可以按照业务随时进行调整
                    if (lock.tryLock(5, TimeUnit.SECONDS)) {
                        // 调用某个方法,这个方法会超时60s(偶现)
                        timeoutApi();
                    } else {
                        /// 你可以写一些业务逻辑,来处理超时的线程2和超时期间的后续线程
                        LOGGER.info("operation timeout");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    // 一定要在finally解锁
                    lock.unlock();
                }
            }).start();
        }
    }

    /**
     * 超时的API
     */
    private static void timeoutApi() {
        LOGGER.info("timeout api start.");
        sleep(60);
        LOGGER.info("timeout api end.");
    }

    /**
     * 设置超时时间
     *
     * @param timeOut 超时时间(秒)
     */
    private static void sleep(long timeOut) {
        try {
            TimeUnit.SECONDS.sleep(timeOut);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

    当然,这是一种处理方式,还有其他的处理方式,欢迎评论区留言。

2. 案例二

(1) 场景

    写了一下公平锁和非公平锁在执行过程中的打印。

(2) 代码:git地址

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLockCase2
 * 演示公平锁和非公平锁区别
 *
 * @author wxy
 * @date 2023-02-16
 */
public class ReentrantLockCase2 {
    private static final Logger LOGGER = LoggerFactory.getLogger(ReentrantLockCase1.class);

    public static void main(String[] args) {
        /*
        公平锁,执行结果:
        Thread-1, num : 1
        Thread-0, num : 1
        Thread-3, num : 1
        Thread-2, num : 1
        Thread-4, num : 1
        Thread-1, num : 2
        Thread-0, num : 2
        Thread-3, num : 2
        Thread-2, num : 2
        Thread-4, num : 2

        5个线程很有规律,你一次我一次。
         */
        ReentrantLock lock = new ReentrantLock(true);

        /*
        非公平锁,执行结果:
        Thread-0, num : 1
        Thread-0, num : 2
        Thread-1, num : 1
        Thread-1, num : 2
        Thread-2, num : 1
        Thread-2, num : 2
        Thread-3, num : 1
        Thread-3, num : 2
        Thread-4, num : 1
        Thread-4, num : 2

        一个线程执行完毕后,下一个线程才能开始执行。为什么出现这样的情况呢?
        非公平锁就是谁能抢谁先来,由于Thread-0线程刚执行完一次,线程还处于活跃状态,
        而其他线程需要被唤醒才能执行,所以相对于其他线程来说他更活跃,所以再次抢到锁并执行。
         */
        // ReentrantLock lock = new ReentrantLock(false);
        for (int index = 0; index < 5; index++) {
            new Thread(() -> {
                for (int num = 1; num <= 2; num++) {
                    lock.lock();
                    try {
                        LOGGER.info("thread name: {}, num : {}", Thread.currentThread().getName(), num);
                    } finally {
                        lock.unlock();
                    }
                }
            }).start();
        }
    }
}

参考文章

    1. ReentrantLock类中的方法

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

到了这里,关于ReentrantLock介绍及使用(超详细)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • java ReentrantLock 锁 await、signal的用法

    在并发编程中,为了保证线程的原子执行,需要使用锁,jvm 内 可以使用 synchronized 和 ReentrantLock,如果是集群部署,我们可以使用Redis 分布式锁 其他的锁后面再介绍。 1、ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与synchronized(1.8之后性能得到提升)会被JVM自动

    2024年02月11日
    浏览(78)
  • 深入浅出 Java 中的神锁:ReentrantLock,还有谁不会?

    来源:jiannan.blog.csdn.net/article/details/121331360 话不多说,扶我起来,我还可以继续撸。 在学习 ReentrantLock 源码之前,先来回顾一下链表、队列数据结构的基本概念~~ 小学一、二年级的时候,学校组织户外活动,老师们一般都要求同学之间小手牵着小手。这个场景就很类似一

    2024年02月08日
    浏览(47)
  • 深入源码解析 ReentrantLock、AQS:掌握 Java 并发编程关键技术

    🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者 📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代 🌲文章所在专栏:JUC 🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识 💬 向我询问任何您想要的

    2024年02月11日
    浏览(51)
  • 【从零开始学习Java重要知识 | 第三篇】暴打ReentrantLock底层源码

    目录 前言: 前置知识:  什么是公平锁与非公平锁? 尝试自己构造一把锁: ReentrantLock源码: 加锁: 解锁: 总结:   在并发编程中,线程安全是一个重要的问题。为了保证多个线程之间的互斥访问和正确的同步操作,Java提供了一种强大的锁机制——ReentrantLock(可重入锁

    2024年01月20日
    浏览(41)
  • 【六大锁策略-各种锁的对比-Java中的Synchronized锁和ReentrantLock锁的特点分析-以及加锁的合适时机】

    阅读该文章之前要了解,锁策略是为了解决什么问题 多线程带来的的风险-线程安全的问题的简单实例-线程不安全的原因 提示:以下是本篇文章正文内容,下面案例可供参考 锁冲突是指两个线程对一个对象加锁,产生了阻塞等待。 乐观锁 假设数据一般情况下不会产生并发冲

    2024年02月15日
    浏览(66)
  • 16.ReentrantLock全解读

    大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:共同富裕的Java人。 经历了AQS的前世和今生后,我们已经知道 AQS是Java中提供同步状态原子管理,线程阻塞/唤醒,以及线程排队功能的同步器基础框架 。那么我们今天就来学习通过

    2024年02月02日
    浏览(37)
  • 面试之ReentrantLock

    ReentrantLock实现了Lock接口,是一个可重入且独占式的锁,和Synchronized类似,不过ReentrantLock更灵活,更强大, 增加了轮询、超时、中断、公平锁和非公平锁等高级功能。 ReentrantLock 里面有一个内部类 ,Sync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实

    2024年02月12日
    浏览(37)
  • 《ReentrantLock与synchronized的区别》

    目录 Synchronized同步锁 synchronized的用法: ReentrantLock ReentrantLock和Synchronized的区别   Synchronized同步锁         使用Synchronized将一段代码锁起来,同一时间只允许一个线程访问。只有获取了这把锁的线程才能访问这段代码,并且只有一个线程拥有这把锁。这样就保证了代

    2024年02月16日
    浏览(35)
  • 从ReentrantLock角度解析AQS

    是它,是它,就是它,并发包的基石; 闲来不卷,随便聊一点。 一般情况下,大家系统中至少也是JDK8了,那想必对于JDK5加入的一系列功能并不陌生吧。那时候重点加入了 java.util.concurrent 并发包,我们简称为JUC。JUC下提供了很多并发编程实用的工具类,比如并发锁lock、原子

    2023年04月14日
    浏览(40)
  • ReentrantLock与synchronized的区别

    synchronized 基于代码块的方式控制加锁与解锁 ReentrantLock 基于 lock(加锁) 与 unlock(解锁) 方式控制加锁与解锁 synchronized 只是单纯的加锁与解锁 ReentrantLock 对于 加锁成功 与synchronized 并无区别 ReentrantLock 对于 加锁失败 提供了额外的 tryLock方法 通过tryLock方法可以直接返回false, 由程

    2023年04月10日
    浏览(62)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包