详述重入锁-ReentrantLock

article/2025/10/14 0:37:28

什么是重入锁?

锁主要用来控制多线程访问的行为,对于同一个线程,如果连续两次对同一把锁进行lock,那么这个线程会被卡死在那里,这样的特性很不好,在实际的开发中,方法之间的调用方式错综复杂,如果不小心可能在多个不同的方法中,反复调用 lock(),这样就会把自己卡死。

所以,重入锁就是用来解决这个问题的,重入锁使同一个线程可以对同一把锁在不释放的前提下,反复的加锁不会导致线程的卡死,唯一的一点就是需要保证 unlock() 的次数和 lock()一样的多。

重入锁的实现

Java中的锁都来自与 Lock 接口,而 ReadWriteLoc k实现的 lock 接口,本文主要分析这两个接口的几个子类的实现细节。
在这里插入图片描述
而重入锁最重要的方法就是lock()。

  • lock():加锁,如果锁已经被别人占用了,就无限等待
  • tryLock(long timeout, TimeUnit unit) throws InterruptedException:尝试获取锁,等待timeout时间。同时,可以响应中断
  • unlock() :释放锁
  • tryLock():不会进行任何等待,如果能够获得锁,直接返回true,如果获取失败,就返回false
  • lockInterruptibly():可以响应中断,lock方法会阻塞线程直到获取到锁
  • newCondition():返回一个条件变量,一个条件变量也可以做线程间通信来同步线程。

重入锁实现原理

重入锁实现的主要类如下图:

在这里插入图片描述
重入锁的核心功能委托给内部类 Sync 实现,并且根据是否是公平锁有 FairSync 和 NonfairSync 两种实现。这是一种典型的策略模式。

实现重入锁的方法很简单,就是基于一个状态变量 state。这个变量保存在AbstractQueuedSynchronizer对象中

private volatile int state;

当 stat e== 0 时,表示锁是空闲的,大于零表示锁已经被占用, 它的数值表示当前线程重复占用这个锁的次数。因此,lock() 的最简单的实现是:

final void lock() {// CAS设置共享状态,返回true表示成功获取共享状态if (compareAndSetState(0, 1))// 设置当前线程为共享状态的持有线程setExclusiveOwnerThread(Thread.currentThread());else// 否则调用AQS中的acquire(int arg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态acquire(1);}

下面是acquire() 的实现:

 public final void acquire(int arg) {//tryAcquire() 再次尝试获取锁,//如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,//同时宣布获得所成功,这正是重入的关键所在if (!tryAcquire(arg) &&// 如果获取失败,那么就在这里入队等待acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果在等待过程中 被中断了,那么重新把中断标志位设置上selfInterrupt();
}

下面我们说一下公平锁 和 非公平锁

公平的重入锁

初始化时, state = 0,表示无人抢占了锁。这时候,这时线程 A 请求锁,获得了锁,把 state + 1,如下所示:

在这里插入图片描述

线程 A 取得了锁,把 state +1,这时候 state 改为 1,线程 A 继续执行其他任务,此时线程B请求锁,线程 B 无法获取锁,生成节点进行排队,如下图所示:

在这里插入图片描述
初始化的时候,会生成一个空的头节点,然后才是线程 B 节点,这时候,如果线程 A 又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当 A 再次请求锁,这时候的状态如下图所示:

在这里插入图片描述
到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程 A 释放了一次锁,如下图所示:

在这里插入图片描述
仅仅是把状态值减了,只有线程 A 把此锁全部释放了,状态值减到 0 了,其他线程才有机会获取锁。当线程 A 把锁完全释放后,state 恢复为 0,然后会通知队列唤醒线程 B 节点,使B可以再次竞争锁。当然,如果线程 B 后面还有线程 C,线程 C 继续休眠,除非 B 执行完了,通知了线程 C。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。

非公平的重入锁

理解了公平锁的话,那非公平锁就容易理解了,当线程 A 执行完之后,要唤醒线程 B 是需要时间的,而且线程 B 醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程 C,那么线程 C 是有可能获取到锁的,如果线程 C 获取到了锁,线程 B 就只能继续等待休眠了。

那公平锁和非公平锁实现的核心区别在哪里呢?如下所示:

//非公平锁 final void lock() {//直接抢了再说if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());else//抢不到,就进队列慢慢等着acquire(1);}//公平锁final void lock() {//直接进队列等着acquire(1);}

我们从代码中可以看到,非公平锁如果第一次争抢失败,后面的处理和公平锁是一样的,都是进入等待队列慢慢等。

而对应tryLock()方法也非常类似的:

 //非公平锁 
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();// 如果当前共享状态未被其他线程占用if (c == 0) {// 尝试通过CAS占有当前共享状态if (compareAndSetState(0, acquires)) {// 设置共享状态持有线程为当前线程setExclusiveOwnerThread(current);return true;}}// 如果共享状态已被占用,则判断当前占用共享状态的线程是否就是当前线程else if (current == getExclusiveOwnerThread()) {// 如果是则自增获取次数,设值stateint nextc = c + acquires;if (nextc < 0) throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}//非公平锁 
protected final boolean tryAcquire(int acquires) {// 获取到当前线程final Thread current = Thread.currentThread();// 获取当前同步状态int c = getState();// 如果同步状态为0,则说明当前同步状态已完全释放if (c == 0) {// 1、hasQueuedPredecessors判断当前节点是否存在前驱节点// 2、如果不存在则CAS设置state的值if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {// 前两个都满足则,设置同步状态持有的线程为当前线程setExclusiveOwnerThread(current);return true;}}// 否则判断当前线程和持有共享状态的线程是否是同一个线程else if (current == getExclusiveOwnerThread()) {// 如果是,重入,状态值增加int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");// 设值新的状态值setState(nextc);return true;}return false;}

引入 Condition

Condition 的作用与 Object.wait() 和 Object.notify() 的作用大致是相同的。但是 wait() 和 notify() 方法是与synchronized 关键字合作使用的,而 Condition 是与重入锁相关联的。通过 Lock 接口(重入锁实现了这一接口)的new Condition() 方法可以生成一个与当前重入锁绑定的 Condition 实例。利用 Condition 对象,可以让线程在合适的时间等待,或者在某一个特定的时间得到通知。

Condition 接口提供的方法

/*** 使当前线程进入等待状态直到被通知(signal)或中断* 当其他线程调用 singal() 或 singalAll() 方法时,该线程将被唤醒* 当其他线程调用 interrupt() 方法中断当前线程* await() 相当于 synchronized 等待唤醒机制中的 wait() 方法*/void await() throws InterruptedException;
//当前线程进入等待状态,直到被唤醒,该方法不响应中断要求
void awaitUninterrruptibly();
 //调用该方法,当前线程进入等待状态,直到被唤醒或被中断或超时//其中 nanosTimeout 指的等待超时时间,单位纳秒
long awaitNanos(long nanosTimeout) throws InterruptedException;
//同 awaitNanos,但可以指明时间单位
boolean await(long time, TmeUnit unit) throws InterruptedException;
 //调用该方法当前线程进入等待状态,直到被唤醒、中断或到达某个时//间期限(deadline),如果没到指定时间就被唤醒,返回 true,其他情况返回 false
boolean await(Date deadline) throws InterruptedException;
//唤醒一个等待在 Condition 上的线程,该线程从等待方法返回前必须
//获取与 Condition 相关联的锁,功能与notify()相同
void signal();
//唤醒所有等待在 Condition 上的线程,该线程从等待方法返回前必须
//获取与 Condition 相关联的锁,功能与 notifyAll() 相同
void signalAll();

代码演示一下 Condition 的使用:

public class ReentrantLockCondition implements Runnable{public static ReentrantLock lock = new ReentrantLock();//通过 ReentrantLock 创建 Condition 实例,并与之关联public static Condition  condition = lock.newCondition();@Overridepublic void run(){try{lock.lock();condition.await();System.out.println("Thread is going on");}catch (InterruptedException e){e.printStackTrace();}finally{lock.unlock();}}public static void main(String[] args) throws InterruptedException {// TODO Auto-generated method stubReentrantLockCondition condition1 = new ReentrantLockCondition();Thread thread= new Thread(condition1 );thread.start();Thread.sleep(2000);lock.lock();condition.signal();lock.unlock();}}

与 Object.wait() 和 Object.notify() 方法类似,当前线程使用 Condition.await() 时,要求线程持有相关的重入锁,在Condition.await() 调用后,这个线程会释放这把锁。同理,在 Condition.signal() 方法调用时,也要求线程先获得相关的锁。在 signal() 方法调用后,系统会从当前 Condtion 对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在 signal() 方法调用之后,一般需要释放相关的锁,谦让给被唤醒的线程,让它可以继续执行。

总结

对于重入锁,这里我们需要知道几点:

  • 对于同一个线程,重入锁允许你反复获得通一把锁,但是,申请和释放锁的次数必须一致
  • 默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平锁
  • 重入锁的内部实现是基于 CAS 操作的
  • 重入锁的伴生对象 Condition 提供了 await() 和 singal() 的功能,可以用于线程间消息通信

http://chatgpt.dhexx.cn/article/gTpyaoAx.shtml

相关文章

进行LDPC编码时扔掉了前面2Zc的信息位,为何如此设计?在解码时如何恢复?

好久没发文章了&#xff0c;拿去年的周报水一篇吧。 其实我也不是研究LDPC的&#xff0c;就是之前被导师提过这个问题&#xff0c;就看了一阵子。下面进入正文—— LDPC基础概念 对于这个问题&#xff0c;还是需要对LDPC码有初步的了解&#xff0c;下面先由一个普通的规则LDPC…

基于matlab的ldpc编码的构造,基于LDPC编码的GMSK调制与解调及matlab仿真实现(含录像)...

基于LDPC编码的GMSK调制与解调及matlab仿真实现(含录像)(开题报告,论文10700字,程序代码,录像) 摘 要 随着无线通信技术的不断发展与进步&#xff0c;数字电视广播、移动视频点播等对数据吞吐量要求很高的业务逐渐变得可能。为了在有限的带宽内用有限的发射功率保证信息在空间传…

ldpcMATLAB/ldpc的译码,matlab程序/LDPC编码的matlab实现/源码

ldpcMATLAB/ldpc的译码,matlab程序/LDPC编码的matlab实现/源码 1、ldpcMATLAB&#xff08;ldpc编码的MATLAB例子&#xff0c;内容相对来说还是比较详细的。值得参考借鉴。&#xff09; 2、LDPCmatlab&#xff08;LDPC码的matlab实现&#xff0c;把解码的部分改过了&#xff…

通信算法之四十:5G NR LDPC编码译码性能仿真

1.码率1/5&#xff0c;增益14dB 2.码率1/12&#xff0c;增益17.8dB [ 相应MATLAB仿真代码。咨询qq&#xff1a;1279682290 ]

DVB-S2系统中LDPC译码器的实现 LDPC编码器的实现 FPGA DVB-S2、DVB-S2X LDPC 编码IP、LDPC译码 IP

DVB S2 LDPC 译码器.IP core IP Core特点&#xff1a; 1.全部代码由FPGA代码实现&#xff1b; 2.支持DVB-S2、DVB-S2X全部格式&#xff1b; 3.支持编码、译码&#xff1b; 4.支持标标准中16200、32400、64800三种帧长&#xff1b; 5.支持ACM、VCM、CCM模式&#xff0c;参…

UCI和数据复用在pusch上传输(第六部分)---ldpc编码

ldpc编码针对的是上下行数据的编码&#xff0c;也是5G最重要的一种编码。 1 TB块添加CRC 这个是和UCI POLAR区别的一个地方&#xff0c;UCI是对每个cb块添加crc. % Get transport block size after CRC attachment according to 38.212% 6.2.1 and 7.2.1, and assign CRC po…

【编码译码】基于matlab实现LDPC编码和解码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

【LDPC编码】CDR系统中LDPC编码,LDPC编码的码长为9216

1.软件版本 matlab2013b 2.系统描述 在广播通信系统中&#xff0c;消息发送的速度和性能是一对矛盾&#xff0c;当发送速度快&#xff0c;则必然会降低消息的可靠性&#xff0c;当要求系统的性能&#xff0c;则必然会降低发送码率。为了提高系统的性能&#xff0c;并尽可能的…

5G LDPC编码流程

参照3GPP的标准文档&#xff0c;摘录其中下行共享信道与寻呼信道的LDPC编码流程&#xff0c;以便于进行对应的软件仿真 传输块加CRC校验   记待传输的数据块序列为 a 0 , a 1 , ⋯ , a A − 1 a_0,a_1,\cdots,a_{A-1} a0​,a1​,⋯,aA−1​&#xff0c;其中 A A A为传输块的负…

linux杀进程

linux学习心得 查看所有进行中的后台进程 命令 &#xff1a;ps aux ps命令查看应用进程。 后面的参数 -a &#xff1a; 显示现行终端机下的所有进程&#xff0c;包括其他用户的进程&#xff1b; -u &#xff1a;以用户为主的进程状态 &#xff1b; x &#xff1a;通常与 a …

Linux找到进程并杀死

第一种情况&#xff1a; 写脚本的时候没没把程序杀死&#xff0c;再次启动发现端口被占用了&#xff0c;于是找到原来的端口然后kill掉重启 用下面2个命令可以&#xff1a; netstat -tunlp|grep 8080t&#xff1a;表示查看tcp u&#xff1a;表示查看udp n&#xff1a;表示端口…

在 Linux 中杀死一个进程

在 Linux 中&#xff0c;假如一个进程的 PID 为 3810&#xff0c;那么结束一个进程可以使用如下命令&#xff1a; $ kill -9 3810 以 Postman 为例&#xff0c;首先我们需要找到它的进程号&#xff0c;然后才能杀死。 查找进程号使用 ps 命令&#xff0c;不过有一个强大的参…

Linux命令行下杀死一个进程

在做项目的时候经常会出现程序死机、锁死、无响应等情况&#xff0c;这时候就需要找到程序相应的进程将其杀掉即可。步骤如下&#xff1a; 1.定位进程 top命令&#xff1a;可以实时动态地查看系统的整体运行情况&#xff0c;是一个综合了多方信息监测系统性能和运行信息的实用…

linux中进程杀不死解决办法

如图&#xff0c;多次在kill -9 此进程后依然存在 先说解决办法&#xff0c;输入过滤命令ps -ef|grep xxx&#xff08;此处写要过滤的进程名字&#xff09;&#xff0c;例如 之后kill掉对应的进程号&#xff0c;如图 问题解决。 如果有多个子进程可以先过滤再一起杀死&#xf…

Linux中kill命令杀不掉进程的解决办法

1、进程杀不掉的原因有两种&#xff1a; &#xff08;1&#xff09;这个进程是僵尸进程 &#xff1b; &#xff08;2&#xff09;此进程是"核心态"进程。 2、解决办法&#xff1a; &#xff08;1&#xff09;进入到“/proc/进程号”目录下&#xff0c;执行“cat stat…

Linux 批量杀进程

1.查需要杀死的进程 ps -ef | grep yum 2.去除掉里面的grep ps -ef | grep yum | grep -v grep 3.打印kill命令 ps -ef | grep yum | grep -v grep | awk {print "kill "$2} 4.执行kill命令 ps -ef | grep yum | grep -v grep | awk {print "kill "$2} …

linux 找出谁杀了进程

目录 Linux Signal 到底是什么信号 OOM 谁发的信号 systemtap audit 案例与总结 服务端的程序&#xff0c;一般都希望比较长时间的运行&#xff0c;比如7*24小时。不过&#xff0c;程序也需要更新&#xff0c;比如修Bug&#xff0c;比如增加新功能&#xff0c;比如修复增…

Linux操作系统之批量杀死进程

前言 在Linux操作系统中&#xff0c;一般常用的杀死进程的命令是 kill 、 pkill 、 killall &#xff0c;根据杀死单个进程拓展至批量杀死进程。 1、查看指定名称的进程&#xff0c;如下查看运行wps程序的进程&#xff1a; ps -ef | grep wps | grep -v grep 结果如下&…

在 VUE中,动态加载JS文件

需求 在vue组件中需要调用的函数方法名是相同的&#xff08;接口相同&#xff09;&#xff0c;但是按照页面不同需要导入不同JS文件 。如下&#xff1a; 然而上面这种写法肯定是行不通的&#xff0c;但表达的需求很明确。根据menuId的不同从JS文件中获取方法 解决方案 promi…

原生js实现动态加载js文件?

一、写在前面 今天拼多多笔试题&#xff0c;题目如下&#xff1a; 实现一个动态加载函数function loadScript(src, attrs)返回Promise, 其中 src是脚本地址&#xff0c;attrs是脚本属性。二、具体实现 <script>function loadScript(src, attrs) {return new Promise((r…