文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

ReentrantLock介绍及使用(超详细)

2023-09-20 21:16

关注

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

目录

一、介绍

1. 简介

2. 是什么类型的锁

3. 优点

4. 原理

5. 主要方法

6. 使用时注意事项

二、实际应用

1. 案例一

2. 案例二


1. 简介

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

2. 是什么类型的锁

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

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

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

    (2) 互斥锁

    一次只能执行一个线程。

    (3) 可重入锁

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

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

3. 优点

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

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

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

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

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

4. 原理

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

00d350814d0a461d8fa563a2181402b5.png

0abbe90e634342748edb486e57b9ea8f.png

669d3f4ca638430aaf370ce98b634bb5.png

129186ac9a744562b0e21da79373f85f.png

edd21c08f7fe409ab252fd846bbc1c0b.png

   点来点去发现,具体的加锁逻辑的实现在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;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 {                    // 设置如果线程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();        }    }        private static void timeoutApi() {        LOGGER.info("timeout api start.");        sleep(60);        LOGGER.info("timeout api end.");    }        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;public class ReentrantLockCase2 {    private static final Logger LOGGER = LoggerFactory.getLogger(ReentrantLockCase1.class);    public static void main(String[] args) {                ReentrantLock lock = new ReentrantLock(true);                // 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://blog.csdn.net/qq_45871274/article/details/129085804

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯