常见的锁策略和synchronized的锁机制

news2024/9/24 3:19:26

文章目录

  • 一. 常见的锁策略
    • 1. 乐观锁和悲观锁
    • 2. 轻量级锁和重量级锁
    • 3. 自旋锁和挂起等待锁
    • 4. 普通互斥锁和读写锁
    • 5. 公平锁和非公平锁
    • 6. 可重入锁和不可重入锁
  • 二. synchronized的锁机制
    • 1. 锁升级/锁膨胀
    • 2. 锁消除
    • 3. 锁粗化

一. 常见的锁策略

1. 乐观锁和悲观锁

乐观锁和悲观锁主要是看主要是锁竞争的激烈程度.

  • 乐观锁预测锁竞争不是很激烈, 做的准备工作相对更少, 开销更小, 效率更高.
  • 悲观锁预测锁竞争会很激烈, 做的准备工作相对更多, 开销更大, 效率更低.

🍂举个例子:

比如在疫情期间, 我们谁也不知道下一步疫情的情况, 疫情一旦严重, 吃饭都成问题, 可能会买不到菜!

悲观锁, 就是在认为当前时刻可能就会出现这样的情况, 就需要提前准备, 所以去超市菜场大量的屯粮屯药, 以备不时之需;

乐观锁, 就是认为在国家的管控下, 疫情正常的衣食不会有太大的影响, 就不去屯货了.

🍂应用场景:

乐观锁一般应用在线程的冲突比较少, 也就是说读操作比较多, 写操作比较少; 而悲观锁则相反, 一般应用在线程冲突比较多, 写操作比较多的情况下.

2. 轻量级锁和重量级锁

轻量级锁和重量级锁看的是锁操作的开销大不大.

  • 重量级锁: 加锁解锁的开销比较的大, 需要在内核态中进行完成, 多数情况下悲观锁是一个重量级锁, 但并不绝对.
  • 轻量级锁: 它的开销比较小, 在用户态中完成就行, 多数情况下乐观锁是一个轻量级锁, 但并不绝对.

轻量级锁一般都是通过版本号机制或CAS算法进行实现的, 但对用重量级锁来说不是, 这与操作系统的内核有关, cpu一般会提供一些特殊指令, 操作系统会对这些指令进行封装一层, 提供一个mutex(互斥量), 在Linux种就会提供一个这样的接口供用户进行加锁解锁; 一般来说, 如果锁是通过调用mutex来进行实现的, 那么这个锁就是一个重量级锁.

3. 自旋锁和挂起等待锁

  • 自旋锁: 是一种典型的轻量级锁, 在线程获取不到锁的情况下, 不会立刻放弃CPU, 而是会一直快速频繁进行获取, 直到获取到锁; 这种策略的优势在于节省线程调度的开销并且更能及时的获取到锁, 缺点就是更加浪费cpu资源.
  • 挂起等待锁: 是一种典型的重量级锁, 线程在获取不到锁的情况下会堵塞等待(放弃CPU,进入等待队列), 然后等到锁被释放的时候再有操作系统调度.

🍂举个例子:

想象一下, 去追求一个女神, 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了.

挂起等待锁: 继续等, 等女神分手, 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?”, 但这个很长的时间间隔后女神想起了你, 这期间已经是沧海桑田了, 女神可能已经换了好几个男票了.

自旋锁: 死皮赖脸的继续追求, 仍然每天持续的和女神说早安晚安, 一旦女神和上一任分手, 那么就能立刻抓住机会上位.

4. 普通互斥锁和读写锁

对于Java中synchronized这样的锁就是一个普通的互斥锁, 只涉及两个操作:

  1. 加锁.
  2. 解锁.

之所以有读写锁的设置是基于一个事实, 多线程针对同一个变量并发读, 这个时候没有线程安全问题的, 也不需要加锁控制, 对于读写锁来说, 分成了三个操作:

  1. 加读锁:如果代码只进行了读操作,就加读锁
  2. 加写锁:如果代码进行了修改操作,就加写锁
  3. 解锁: 针对读锁和读锁之间,是不存在互斥关系的

读锁和读锁之间是没有互斥的, 读锁和写锁之间, 写锁和写锁之间, 才需要互斥

在java中有读写锁的标准类, 位于java.util.concurrent.locks.ReentrantReadWriteLock, 其中ReentrantReadWriteLock.ReadLock为读锁, ReentrantReadWriteLock.WriteLock为写锁.

🍂应用场景:

很多开发场景中, 读操作非常高频, 比写操作的频率高很多, 如果我们所有场景都使用同一种锁, 就会浪费一些资源, 而如果使用读写锁将读操作和写操作分开加锁, 就可以避免一些不必要的开销.

5. 公平锁和非公平锁

  • 公平锁: 多个线程在等待一把锁的时候, 遵循先来后到原则, 谁是先来的, 谁就先获得这把锁.
  • 非公平锁: 多个线程等待同一把锁, 不遵守先来后到原则, 每个人等待线程获取锁的概率是均等的.

操作系统中原生的锁都是 “非公平锁”, 操作系统中的针对加锁的控制, 是依赖于线程调度顺序的, 这个调度顺序是随机的, 不会考虑到这个线程等待锁多久了, 如果想要实现公平锁, 就得在这个基础上加额外的数据结构(比如引入一个队列), 来记录线程的先后顺序.

6. 可重入锁和不可重入锁

  • 可重入锁: 一个线程针对同一把锁连续加锁两次,不会出现死锁
  • 不可重入锁: 一个线程针对同一把锁连续加锁两次,会出现死锁

二. synchronized的锁机制

结合上面的锁策略, Synchronized 具有如下特性 性(只考虑 JDK 1.8):

  • 既是乐观锁也是悲观锁, 当锁竞争较小时它就是乐观锁(默认), 锁竞争较大时它就是悲观锁.
  • 是普通互斥锁。
  • 既是轻量级锁(默认)也是重量级锁, 根据锁竞争激烈程度自适应.
  • 轻量级锁部分基于自旋锁实现, 重量级锁部分基于挂起等待锁实现.
  • 是非公平锁.
  • 是可重入锁.

1. 锁升级/锁膨胀

JVM 将 synchronized 锁分为 无锁, 偏向锁, 轻量级锁, 重量级锁 状态; 会根据情况, 进行依次升级.

img

  1. 当没有线程加锁的时候, 此时为无锁状态.
  2. 当首个线程进行加锁的时候, 此时进入偏向锁的状态, 偏向锁不是真的加锁, 而是在对象头做个标记(这个过程是非常轻量的).

🍂举个例子理解偏向锁:

这里我的人设是一个妹子, 我谈了一个小哥哥, 长的又帅又有钱, 如果时间长了, 就想换换把他甩了, 但是他要是对我纠缠不休, 这就很麻烦.

于是我就调整了更高效谈恋爱的方式, 我就只是和这个小哥哥搞暧昧, 不明确我们彼此的关系, 这样做的好处就是有朝一日, 我想换男朋友了, 就直接甩了就行, 毕竟我们有情侣之实而无情侣之名, 这样换人的成本就很低了.

但是如果在这个过程中, 有另外一个妹子, 也在对这个小哥哥频频示好, 我就需要提高警惕了, 对于这种情况, 由于我和小哥哥前面感情铺垫到位了, 就可以立即顺理成章的和小哥哥官宣确认情侣关系(加锁), 并且勒令小哥哥和这个妹子离远点.

  • 偏向锁并不是真的加锁, 只是做了一个标记, 这样带来的好处就是, 后续如果没人竞争的时候, 就一直不去确立关系(节省了确立关系/分手的开销), 如果没有其他的线程来竞争这个锁, 就不必真的加锁(节省了加锁解锁的开销), 如果在执行代码过程中, 并没有其他线程来尝试加锁, 那么在执行完synchronized之后, 取消偏向锁即可.

上述过程, 就类似于 “偏向锁” 这个过程, 相当于 “懒汉模式” 中的懒加载一样, “非必要,不加锁”.

  1. 当有其他线程进行加锁, 导致产生了锁竞争时, 此时进入轻量级锁状态(迅速的把偏向锁升级成真正的加锁状态).

此处的轻量级锁是通过 CAS 来实现的, 如果其他的线程很快的释放锁, 那么我们的自旋锁是非常合适的, 但是如果其他线程长时间占用锁, 自旋锁就不太合适了, 自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源, 因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了, 也就是所谓的 “自适应”.

  1. 如果竞争进一步加剧, 进入重量级锁状态(挂起等待锁).

重量级锁是基于操作系统原生的一组API(mutex)来进行加锁了, 当我们自旋不能快速获取到锁时, 锁竞争加剧, 就会升级为重量级锁, 我们的线程就会被放到阻塞队列中, 暂时不参与CPU调度, 当锁被释放了之后, 线程才有机会被调度, 从而有机会获取到锁, 一旦线程被切换出cpu, 这就是比较低效的事情了.

2. 锁消除

锁消除是编译器的智能判定, 有些代码, 编译器认为没有加锁的必要, 就会自动把你加的锁自动去除, 比如字符串相关的线程安全类StringBuffer, 这个类中的关键方法都带有synchronized, 但是当我们在单线程环境下使用StringBuffer, 不会涉及到线程安全问题, 此时编译器就会直接把这些加锁操作去除了.

3. 锁粗化

锁粗化就是将synchronized的加锁代码块范围增大, 加锁的代码块中的代码越多, 锁的粒度就越粗, 否则锁的粒度就越细.

通常情况下, 认为锁的粒度细一点比较好, 加锁的部分的代码, 是不能并发执行的, 锁的粒度越细, 能并发的代码就越多, 反之就越少.

但是有些情况下, 锁的粒度粗一些反而更好, 如果我们两次加锁解锁的时间间隙非常小, 分开加锁会造成额外的资源开销, 而且中间间隙很小, 就算并发效果也不是很明显, 这种情况下不如直接一把大锁搞定.

🍂举个例子理解锁粗化 :

下属向领导汇报工作:

方式一:

第一次打电话, 汇报工作1, 挂电话.

第二次打电话, 汇报工作2, 挂电话.

第三次打电话, 汇报工作3, 挂电话.

方式二:

打电话, 汇报工作1, 2, 3, 挂电话, 显然, 相对于方式以, 这里的方案更高效.

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

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

相关文章

C++ | 数据结构与算法 | 最小生成树算法讲解 | Kruskal Prim

文章目录前言Kruskal算法Prim算法前言 讲解之前,我们需要先明白连通图是指什么?连通图具有以一个顶点为起点可以到达该图中的任意一个顶点的特性,就算它们不直接相连,但是它们之间至少有一条可以递达的路径。并且连通图是针对无向…

劳务派遣协议范本整理版模板范本

劳务派遣协议范本整理版 甲方(用工单位):______ 法定代表人:____________ 地址:__________________ 电话:__________________ 传真:__________________ 乙方(派遣单位&#xf…

【Java】一文彻底弄懂访问修饰符(public/protected/默认/private)--建议收藏

博主简介:努力学习的预备程序媛一枚~博主主页: 是瑶瑶子啦所属专栏: Java岛冒险记【从小白到大佬之路】 前言 OOP(Object Oriented Programing),即面向对象编程,最重要的功能/特点之一就是封装,这点在该专栏开篇博客【…

WebPack面试题汇总

1,Webpack有什么作用,谈谈你对它的理解 现在的前端网页功能丰富,特别是SPA(single page web application 单页应用)技术流行后,JavaScript的复杂度增加和需要一大堆依赖包,还需要解决Scss、Les…

记录--这样封装列表 hooks,一天可以开发 20 个页面

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 这样封装列表 hooks,一天可以开发 20 个页面 前言 在做移动端的需求时,我们经常会开发一些列表页,这些列表页大多数有着相似的功能:分页获取列表、上拉加载、下拉刷…

【Java 面试题合集】ThreadPoolExecutor 线程池面试题

文章目录自定义的线程池的 7 个参数如何合理设置核心线程数 corePoolSize 的大小《JAVA 并发编程实战》中的方案java 开发手册中为什么不允许使用 Executors 默认的实现?一个线程池中的线程异常了,那么线程池会怎么处理这个线程?线程池被创建后里面有线…

OpenCV——总结《图像处理-1》

1.HSV H - 色调(主波长)。S - 饱和度(纯度/颜色的阴影)。V值(强度) hsvcv2.cvtColor(img,cv2.COLOR_BGR2HSV)2.图像阈值 函数介绍: ret, dst cv2.threshold(src, thresh, maxval, type) sr…

嵌入式Linux从入门到精通之第九节:系统编程

系统编程概述 在讲解系统编程之前,先了解几个概念: 操作系统的作用: 操作系统用来管理所有的资源,并将不同的设备和不同的程序关联起来。 什么是Linux系统编程? 在有操作系统的环境下编程,并使用操作系统提供的系统调用及各种库,对系统资源进行访问。 学会了C语言再知…

Grafana9.3.x在windows上的安装及使用

Grafana9.3.x的安装及使用1. Grafana install1.1 Download1.2 Install2. User Guide1.1 Document1.2 Table视图背景色渲染3.Awakening1. Grafana install 1.1 Download 下载地址 Grafana Website: https://grafana.com/. 1.2 Install 直接点击安装就好了 进入conf目录复制一…

python集合语法与应用

python集合语法与应用 文章目录python集合语法与应用一、实验目的二、实验原理三、实验环境四、实验内容五、实验步骤1.创建2.增加3.删除4.集合运算5.拓展知识一6.拓展知识二总结一、实验目的 掌握集合的用法 二、实验原理 集合中只能包含数字、字符串、元组等不可变的类型的…

规则引擎,实现业务低代码开发的重要工具

规则引擎,是将业务执行抽象化的配置,通过其定义的数据结构、算法和流程来实现应用程序功能的普适化。 规则引擎可以帮助企业提高业务开发效率,提高运营的灵活性,降低运营成本与开发成本,让系统更加智能化灵活化。这里以…

【Rust】5. 所有权

5. 所有权 5.1 什么是所有权 5.1.1 栈(Stack)与堆(Heap) 5.1.2 所有权规则 5.1.3 变量作用域 5.1.4 String 类型 String 类型可进行修改,而字符串字面值是不可以的!(区别在于二者对内存的处理…

OAuth2

目录一、什么是OAuth2.0二、OAuth2中的角色三、认证流程四、生活中的Oauth2思维5. 令牌的特点6.OAuth2授权方式6.1 授权码6.2 隐藏方式6.3 密码方式6.4 凭证方式一、什么是OAuth2.0 OAuth2.0是目前使用非常广泛的授权机制,用于授权第三方应用获取用户的数据。 举例…

PythonWeb Django框架学习笔记

文章目录Django一、初步了解Django1.1 创建项目1.2 文件介绍1.3 APP的创建和说明添加新的app注册app创建页面1.4 templates模板templates语法单一变量列表循环【列表】字典循环【字典】列表套字典条件判断templates小结1.5 请求和响应案例:用户管理二、数据库操作2.…

前言技术之mybatis-plus 01

目录 1.什么是mybatis-plus 2.初体验 3.日志 4.主键生成策略 5.更新 6.自动填充 1.什么是mybatis-plus 升级版的mybatis,目的是让mybatis更易于使用, 用官方的话说“为简化而生” 官网: MyBatis-Plus 2.初体验 1.准备数据库脚本 数据…

再获殊荣!天云数据入选第一批北京市级企业技术中心,Hubble数据库提供新一代信息技术科技服务

为助力北京国际科技创新中心建设,贯彻落实北京市“十四五”时期高精尖产业发展规划,引导和支持企业加强创新能力,培育和引导企业技术中心建设,北京市经济和信息化局组织开展了2022年度第一批北京市市级企业技术中心的创建工作&…

【Python学习笔记】5. Python3 基本数据类型(上)——数值型、字符串型

前言 Python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。 Python3 基本数据类型 Python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。 在 Python 中,变量就是变…

tomcat配置多个host,并且避免重复加载

目录 1.实验描述 2.实验环境 2.实验过程 2.1 创建order和user项目 2.2 打包项目 2.3 修改tomcat的server.xml的配置 2.4 启动tomcat 2.5 配置本地host 2.6 验证 1.实验描述 目前有两个域名,分别是: order.abc.com user.abc.com 两个项目&…

微信小程序学习第1天:微信小程序开发入门介绍

前言:微信小程序开发模式 1、申请小程序开发账号 2、安装小程序开发者工具 3、创建和配置小程序项目 一、申请小程序开发账号 1、体验小程序 2、注册小程序开发账号 使用浏览器打开https://mp.weixin.qq.com网址,按照提示注册 注册承购后&#xff0c…

微服务 初始 分布式搜索引擎 Elastic Search

文章目录⛄引言一、什么是Elastic Search?二、Elastic Search 倒排索引⛅正向索引⚡倒排索引⛄正向和倒排三、ES的一些概念⛅文档和字段⚡索引和映射四、MySQL 与 Elasticsearch⛵小结⛄引言 本文参考黑马 分布式Elastic search Elasticsearch是一款非常强大的开源…