ReentrantLock底层原理学习一

J.U.C 简介

Java.util.concurrent 是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件。比如线程池、阻塞队列、计时器、同步器、并发集合等等。并发包的作者是大名鼎鼎的 Doug Lea。我们在接下来的课程中,回去剖析一些经典的比较常用的组件的设计思想

Lock

Lock 在 J.U.C 中是最核心的组件,前面我们讲 synchronized 的时候说过,锁最重要的特性就是解决并发安全问题。为什么要以 Lock 作为切入点呢?如果有同学看过 J.U.C 包中的所有组件,一定会发现绝大部分的组件都有用到了 Lock。所以通过 Lock 作为切入点使得在后续的学习过程中会更加轻松。

Lock 简介

在 Lock 接口出现之前,Java 中的应用程序对于多线程的并发安全处理只能基于synchronized 关键字来解决。但是 synchronized 在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在 Java5 以后,Lock 的出现可以解决synchronized 在某些场景中的短板,它比 synchronized 更加灵活。

Lock 的实现

Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。实现 Lock 接口的类有很多,以下为几个常见的锁实现
ReentrantLock:表示重入锁,它是唯一一个实现了 Lock 接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数
ReentrantReadWriteLock:重入读写锁,它实现了 ReadWriteLock 接口,在这个类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都分别实现了 Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是: 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
StampedLock: stampedLock 是 JDK8 引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。stampedLock 是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。

Lock 的类关系图

Lock 有很多的锁的实现,但是直观的实现是 ReentrantLock 重入锁

在这里插入图片描述
void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放
void lockInterruptibly() // 和lock()方法相似, 但阻塞的线程 可 中 断 , 抛 出java.lang.InterruptedException 异常
boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回 true
boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法
void unlock() // 释放锁

ReentrantLock 重入锁

重入锁,表示支持重新进入的锁,也就是说,如果当前线程 t1 通过调用 lock 方法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。synchronized 和 ReentrantLock 都是可重入锁。锁会存在重入的特性,那是因为对于同步锁的理解程度还不够,比如在下面这类的场景中,存在多个加锁的方法的相互调用,其实就是一种重入特性的场景。

重入锁的设计目的

比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用demo2,demo2 中的存在同一个实例锁,这个时候当前线程会因为无法获得demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。
public class ReentrantDemo{public synchronized void demo(){System.out.println("begin:demo");demo2();}public void demo2(){System.out.println("begin:demo1");synchronized (this){}}public static void main(String[] args) {ReentrantDemo rd=new ReentrantDemo();new Thread(rd::demo).start();}
}

ReentrantLock 的使用案例

public class AtomicDemo {private static int count=0;static Lock lock=new ReentrantLock();public static void inc(){lock.lock();try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}count++;lock.unlock();}public static void main(String[] args) throws 
InterruptedException {for(int i=0;i<1000;i++){new Thread(()->{AtomicDemo.inc();}).start();;}Thread.sleep(3000);System.out.println("result:"+count);}
}

ReentrantReadWriteLock

我们以前理解的锁,基本都是排他锁,也就是这些锁在同一时刻只允许一个线程进行访问,而读写所在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读线程和其他写线程都会被阻塞。读写锁维护了一对锁,一个读锁、一个写锁; 一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量.
public class LockDemo {static Map<String,Object> cacheMap=new HashMap<>();static ReentrantReadWriteLock rwl=new 
ReentrantReadWriteLock();static Lock read=rwl.readLock();static Lock write=rwl.writeLock();public static final Object get(String key) {System.out.println("开始读取数据");read.lock(); //读锁try {return cacheMap.get(key);}finally {read.unlock();}}public static final Object put(String key,Object value){write.lock();System.out.println("开始写数据");try{return cacheMap.put(key,value);}finally {write.unlock();}}
}
在这个案例中,通过 hashmap 来模拟了一个内存缓存,然后使用读写所来保证这个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被阻塞,因为读操作不会影响执行结果。
在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性
⚫ 读锁与读锁可以共享
⚫ 读锁与写锁不可以共享(排他)
⚫ 写锁与写锁不可以共享(排他)

ReentrantLock 的实现原理

我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。在 synchronized 中,我们分析了偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。那么在 ReentrantLock 中,也一定会存在这样的需要去解决的问题。就是在多线程竞争重入锁时,竞争失败的线程是如何实现阻塞以及被唤醒的呢?
AQS 是什么 
在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。如果你搞懂了 AQS,那么 J.U.C 中绝大部分的工具都能轻松掌握。
AQS 的两种功能 从使用层面来说,AQS 的功能分为两种:独占和共享
独占锁,每次只能有一个线程持有锁,比如前面给大家演示的 ReentrantLock 就是以独占方式实现的互斥锁
共 享 锁 , 允 许 多 个 线 程 同 时 获 取 锁 , 并 发 访 问 共 享 资 源 , 比 如ReentrantReadWriteLock。
AQS 的内部实现 
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

在这里插入图片描述
Node 的组成
在这里插入图片描述
释放锁以及添加线程对于队列的变化
当出现锁竞争以及释放锁的时候,AQS 同步队列中的节点会发生变化,首先看一下添加节点的场景。

在这里插入图片描述
里会涉及到两个变化
1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己
2. 通过 CAS 讲 tail 重新指向新的尾部节点head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下:

这个过程也是涉及到两个变化
1. 修改 head 节点指向下一个获得锁的节点
2. 新的获得锁的节点,将 prev 的指针指向 null
设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/601439.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vite4项目中,vant兼容750适配

一般非vite项目&#xff0c;使用postcss-px-to-viewport。在设计稿为750时候&#xff0c;可使用以下配置兼容vant。 在vite4项目中&#xff0c;以上配置不行。需要调整下&#xff0c;使用postcss-px-to-viewport-8-plugin&#xff0c;并修改viewportWidth&#xff0c;具体如下…

CSS新增文本样式(完整)

文本样式 概念&#xff1a;在CSS3中&#xff0c;增加了丰富的文本修饰效果&#xff0c;使得页面更加美观舒服。 常用的文本样式属性 属性说明text-shadow文本阴影text-stroke文本描边text-align-last文本对齐white-space处理空白字符text-overflow文本溢出word-wrap | word-…

【Linux 内核源码分析】关于Linux内核源码目录结构

Linux内核源码采用树形结构。功能相关的文件放到不同的子目录下面&#xff0c;使程序更具有可读行。 使用Source Insight打开源码&#xff0c;如下图所示&#xff0c;可以看到源码是树形结构。 目录含义描述arch存放与体系结构相关的代码&#xff0c;包括不同硬件平台的特定代…

Qt/QML编程学习之心得:QSocketNotifier(二十一)

QSocketNotifier在Qt中怎么使用? QSocketNotifier使Qt的事件循环与其他基于文件描述符的事件循环集成成为可能。在Qt的主事件循环(QCoreApplication::exec())中检测到文件描述符操作。 使用低级(通常是特定于平台的)API打开设备后,可以创建一个套接字通知程序来监视文…

DDoS攻击的多种方式

DDOS攻击指分布式拒绝服务攻击&#xff0c;即处于不同位置的多个攻击者同时向一个或数个目标发动攻击&#xff0c;或者一个攻击者控制了位于不同位置的多台机器并利用这些机器对受害者同时实施攻击。由于攻击的发出点是分布在不同地方的&#xff0c;这类攻击称为分布式拒绝服务…

leetcode:2451. 差值数组不同的字符串(python3解法)

难度&#xff1a;简单 给你一个字符串数组 words &#xff0c;每一个字符串长度都相同&#xff0c;令所有字符串的长度都为 n 。 每个字符串 words[i] 可以被转化为一个长度为 n - 1 的 差值整数数组 difference[i] &#xff0c;其中对于 0 < j < n - 2 有 difference[i]…

【unity小技巧】FPS游戏实现相机的偏移震动、武器射击后退和后坐力效果

最终效果 文章目录 最终效果前言相机偏移震动相机震动脚本换弹节点震动 武器射击后退效果武器后坐力效果完结 前言 关于后坐力之前其实已经分享了一个&#xff1a;FPS游戏后坐力制作思路 但是实现起来比较复杂&#xff0c;如果你只是想要简单的实现&#xff0c;可以看看这个&…

使用printJS使网页打印成PDF、网页html结合printJS导出为pdf

先放几个参考链接 感谢&#xff01; Vue使用PrintJS实现页面打印功能_vue print.js 设置打印pdf的大小-CSDN博客 前台导出pdf经验汇总 &#xff08;html2canvas.js和浏览器自带的打印功能-print.js&#xff09;以及后台一些导出pdf的方法_iqc后台管理系统怎么做到导出pdf-CSD…

大创项目推荐 深度学习图像分类算法研究与实现 - 卷积神经网络图像分类

文章目录 0 前言1 常用的分类网络介绍1.1 CNN1.2 VGG1.3 GoogleNet 2 图像分类部分代码实现2.1 环境依赖2.2 需要导入的包2.3 参数设置(路径&#xff0c;图像尺寸&#xff0c;数据集分割比例)2.4 从preprocessedFolder读取图片并返回numpy格式(便于在神经网络中训练)2.5 数据预…

ssm基于JSP的网络游戏交易系统的设计与实现+jsp论文

摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;商品管理信息因为其管理内容繁杂&#xff0c;管理数量繁多导致手工进行处理不能满足…

面试题理解深层次的数组名

目录 引言 一&#xff1a;一维数组 举例如下 1.铺垫知识 数组名是数组首元素的地址&#xff0c;但是有两个特殊情况 &#xff08;1&#xff09;sizeof(数组名) &#xff08;2&#xff09;&数组名 2.分析讲解上述代码结果 2.字符数组 举例一如下 1.知识铺垫 …

(三)其他的输入输出

文章目录 getchar();单个字符输入使用&#xff1a; putchar();单个字符输出(自带换行)使用 puts();字符串输出与printf区别使用 gets();后面补充 代码现象 getchar(); 单个字符输入 使用&#xff1a; 变量 getchar(); 例&#xff1a;char a&#xff1b; a getchar(); put…

软件测试之单元测试、系统测试、集成测试知识总结

一、单元测试的概念 单元测试是对软件基本组成单元进行的测试&#xff0c;如函数或一个类的方法。当然这里的基本单元不仅仅指的是一个函数或者方法&#xff0c;有可能对应多个程序文件中的一组函数。 单元也具有一些基本的属性。比如&#xff1a;明确的功能、规格定义&#…

PHP进阶-实现网站的QQ授权登录

授权登录是站点开发常见的应用场景&#xff0c;通过社交媒体一键授权可以跳过注册站点账户的繁琐操作。本文将讲解如何用PHP实现QQ授权登录。首先&#xff0c;我们需要申请QQ互联开发者账号获得APPID和密钥&#xff1b;接着&#xff0c;我们下载QQ官方SDK&#xff1a;PHP SDK v…

IP地址定位技术的应用及其重要性

随着网络技术的快速发展&#xff0c;网络安全问题日益凸显&#xff0c;IP地址定位技术在网络安全领域的应用也越来越广泛。本文将介绍IP地址定位技术在网络安全领域的应用及其重要性。 一、IP地址定位技术概述 IP地址定位技术是指通过一定的技术手段&#xff0c;将虚拟网络中的…

关于《码农翻身》一书的读后感以及自己的一些拙见汇总

书籍名称 《码农翻身》 | 刘欣&#xff08;码农翻身&#xff09; 著 | 文章将以问答的形式进行叙述 1.是从什么渠道接触到《码农翻身》的 一个工作日的下午&#xff0c;手上的任务基本结束&#xff0c;翻了翻桌上的书和笔记之类的&#xff0c;同事见我在看书&#xff0c;于是向…

阿里后端实习一面面经

阿里后端实习一面面经 项目中使用到了es&#xff0c;es的作用&#xff1f; elasticsearch是一款非常强大的开源搜索引擎&#xff0c;具备非常多强大功能&#xff0c;可以帮助我们从海量数据中快速找到需要的内容 es中的重要概念&#xff1f; 群集&#xff1a;一个或多个节点…

PostgreSQL的常见错误和解决方法

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 在学习新的东西时&#xff0c;会犯很多的错误&#xff0c;会遇到很多坑。我们在填坑与犯错中不断进步成长。 以下是在学习pgsql中…

Pytorch详细安装过程

1、安装anaconda 官网&#xff08;https://www.anaconda.com/products/distribution#Downloads&#xff09;下载&#xff0c;使用管理员身份运行&#xff08;不使用似乎也没事&#xff09; 这里选择Just me&#xff08;至于为啥&#xff0c;咱也不是很清楚&#xff09; 更改路…

编程基础 - 初识shell

编程基础 - 初识shell 返回序言及专栏目录 文章目录 编程基础 - 初识shell前言一、Linux的路径1、绝对路径2、相对路径3、特殊路径 二、交互式工作1、先联系上shell2、交互式命令 三、执行脚本四、sh和bash的区别总结 前言 shell是Linux内核外的一层壳&#xff0c;是用户与Li…