当前位置: 首页 > news >正文

14. 多线程(进阶1) --- 常见的锁策略和锁的特性

文章目录

  • 前言
  • 一. 常见的锁策略
    • 1.1. 悲观锁 VS 乐观锁
    • 1.2. 重量级锁 VS 轻量级锁
    • 1.3. 挂起等待锁 VS 自旋锁
    • 1.4. 普通互斥锁 VS 读写锁
    • 1.5. 可重入锁 VS 不可重入锁
    • 1.6. 公平锁 VS 非公平锁
  • 二. 锁的特性
    • 2.1. 锁升级
    • 2.2. 锁清除
    • 2.3. 锁粒度


前言

知识回顾:我们之前学习了 多线程 初阶,讲解了:

  1. 线程原理,进程和线程的关系。
  2. 多线程 Thread 类的使用
  3. 线程安全问题
  4. 等待通知机制 (wait notify)
  5. 多线程代码案例 — 单例模式,生产者-消费者模型,定时器以及线程池

从这篇博客开始,我们主要要讲解 多线程进阶的学习了,这个内容会更加的丰富,主要包含 面试题,咱们开始吧!


一. 常见的锁策略

如果自己要实现一把锁,认为 标准库中提供的锁 不够用,此时就需要关注锁策略。
此处的“锁”策略 不是和 Java 强相关的,但凡涉及到并发编程,涉及到锁,都可以谈到此处的 锁策略。
锁策略主要探讨的就是,不同锁之间在加锁的时候,有什么行为,有什么特点。
虽然 synchronized 的功能和用法已经非常好,可以满足大部分场景的需求,但是架不住 面试官要问。

1.1. 悲观锁 VS 乐观锁

此处的 “乐观” 和 “悲观” 可不是 人的所谓 “乐天派” 或者 “苦瓜脸”,而是针对不同的场景。并且 不是针对某一种具体的锁,而是某个具体的锁具有 “悲观” 特性 或者 “乐观” 特性。
悲观:加锁的时候,预测接下来的锁竞争的情况 非常激烈,就需要针对这样的 激励情况 额外 做一些工作。
乐观:加锁的时候,预测接下来的锁竞争情况 不激烈,就不需要做额外的工作。
有两个例子:

  1. 悲观锁:有一把锁,有二十个线程尝试获取锁,每个线程加锁的频率都很高,一个线程加锁的时候,很可能被另一个线程占用的。
    乐观锁:有一把锁,只有两个线程尝试获取到这个锁,每个线程加锁的频率都很低,一个线程加锁的时候,大概率另一个线程没有和他竞争。
  2. 现在 同学A 和 同学B 想请教老师一个问题:
    同学 A 认为 “老师现在应该比较忙,我先发个微信,看看老师的回应”,然后就给老师说:“老师,你下午忙吗,我下午2点钟想问个问题” (相当于加锁操作)。在得到肯定答复之后,同学A在去办公室找老师;可能老师上午没有看消息,同学A隔了一会,在向老师发出请求。这个就是 悲观锁
    同学 B 认为 “老师现在在休息,现在可以直接去办公室找他问问题”,因此同学 B 直接就去找老师了 (没加锁,直接访问资源)如果老师没有事,那么问题就能立马解决,如果老师确实是很忙,那么同学 B 就不打扰老师,下次再来 (虽然没加锁,但是能识别出数据访问冲突)。这个是个 乐观锁

而在 synchronized中 ,初始使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略

这就好比 有一个 同学 C,他也想问问题,直接去找老师(乐观锁的策略),但是找了两三次之后,发现去了两三次,老师仍然很忙,于是下次再问问题的时候,先发个消息问问老师忙不忙,然后再决定是否来问问题。

1.2. 重量级锁 VS 轻量级锁

遇到场景之后的解决方案
重量级锁,当悲观的场景下,此时就要付出更多的代价 --> 更低效。
轻量级锁,应对乐观的场景下,此时付出的代价就会更小 —> 更高效

这个重量级锁 和 轻量级锁的先搁置,等把 常见的锁策略 讲完之后,再讲这个内容。

1.3. 挂起等待锁 VS 自旋锁

挂起等待锁 --> 重量级锁的典型实现 --> 操作系统内核级别的,加锁的时候发生了锁竞争,就会使线程进入到阻塞状态,后续就需要 内核 来唤醒。
自旋锁 --> 轻量级锁的典型实现 --> 应用程序级别的,加锁的时候发生竞争,一般也不是进入到阻塞,而是通过 忙等 来等待。

忙等,在乐观的时候,本来遇到锁竞争的概率就小,真的遇到竞争,在短时间内能拿到锁。

用两个例子来解释一下:

  1. 追女神,心里的感受是:女神,是大家心中的女神 --> 悲观的态度。
    挂起等待锁的情况下,你去问女神,做我女朋友好不好,女神说,你是个好人。你表示,“没关系,我愿意去等”,等的过程中,不再联系女神,安心敲代码,几个月过去了。偶然间,听说,女神单身了(可能在你听到这个消息的时候,人家都谈了好几次了),此时再去找女神,你愿意做我的女朋友吗?此时女神说,愿意~。
    那么在这个过程中,我在敲代码的时候,我并没有在女神身上消耗精力。
    还有一种感受,就是女神,只是我心中的女神 --> 乐观的态度。
    自旋锁方式,今天问问女神,做我的女朋友好不,女神说,你是个好人。此时,你表示,没事,我愿意等 (甘当备胎),等的过程中,隔三差五的,联系一下,嘘寒问暖,很快,有一天,得知女神和她的男朋友分手了,此时机会来了,你就乘机上位,就得到了女神。
    在这个过程中,并没有消耗很长时间,但是消耗了一定量的 CPU资源的
  2. 在实际的代码场景下,挂起等待锁:在操作系统中,让线程阻塞,后续由系统唤醒,比如首,线程1 在 14:00的时候,因为加锁失败,进入到阻塞状态,14:05 的时候,对应的锁就释放了。14:10的时候,操作系统才唤醒线程1,线程1才拿到锁。
    在 14:00~14:10 的这个过程中,线程1 全程阻塞,不消耗 CPU资源。
    自旋锁,不涉及到内核操作。线程2 在14:00 的时候,因为加锁失败,等待,并不会放弃 CPU资源。
    14:01 问问,锁是否可以加,还是不能加。
    14:02 问问,锁是否可以加,还是不能加。
    14:03 问问,锁是否可以加,还是不能加。

    14:05,其他线程释放了锁,当前线程2 就可以加锁了。
    不涉及到系统内核中的线程调度,第一时间拿到锁。

挂起等待锁的特点:

  • 获取锁的周期很长,很难做到及时获取
  • 获取锁的过程不必一直消耗 CPU 资源
  • 竞争激烈
  • 操作内核操作

自旋锁的特点:

  • 整个锁等待的时间,不会很长
  • 获取锁的周期很短
  • 这个过程会一直消耗 CPU 资源
  • 锁竞争不激烈
  • 应用程序内的

悲观锁 --> 重量级锁 --> 挂起等待锁
乐观锁—> 轻量级锁 --> 自旋锁

synchronized 是挂起等待锁 还是 自旋锁?
其实是,既是 挂起等待锁 又是自旋锁。
设计 JVM 的大佬们,为程序员操碎了心。大佬的初心是,别让程序员操作这些事情,都在 JVM 内部做好。
JVM 内部,会统计每个锁竞争的激烈程度,
如果竞争不激烈,那么此时 synchronized 就会按照 轻量级锁 (自旋),如果竞争激烈,那么此时 synchronized 就会按照 重量级锁 (挂起等待锁)。


1.4. 普通互斥锁 VS 读写锁

读锁就是在读取内容的时候,进行加锁;
写锁就是在往内存中写入数据的时候,进行加锁。
我们知道,多个线程读取一个数据,线程是安全的,多个线程读取一个内容,一个线程修改同一个内容,肯定会涉及到线程安全的问题。
大部分操作在 读,少数操作是在写。
如果把 每个读和写 都加上普通的互斥锁,意味着锁冲突非常严重。
这是个读多,写少的情况,在服务器开发中,是非常常见的情况,或者在教务系统中,通常都是老师布置一项作业,一个人在写的时候,几十个同学在读。

读写锁,确保,读锁和读锁之间,不是互斥的(不会产生阻塞),读锁和写锁之间,才产生互斥。写锁和 写锁之间,也会产生互斥。

读写锁,就是为了降低锁冲突的概率,提高运行效率提出来的。
多线程读一个变量,本身就是线程安全的,不需要加锁,也不会互斥。但是写操作,为了避免写操作对读操作的影响,于是给读操作也加上锁。
synchronized是个 普通互斥锁,Java的标准库中实现了读写锁,这个我们再下一个博客中讲解。


1.5. 可重入锁 VS 不可重入锁

可重入锁的字面意思是:“可以重新进入的锁”,即** 允许同一个线程多次获取同一把锁**,一个线程,一把锁,连续加锁多次,看是否会构成死锁。
核心要义:

  • 锁要记录当前是哪个线程 拿到了这把锁
  • 使用计数器,记录当前加锁了多少次,在合适的时候进行解锁

synchronized 是个 可重入锁,我们可以看一下下面的代码,就知道了。

public class Demo46 {public static void main(String[] args) {Object locker = new Object();int i = 0;synchronized (locker){synchronized (locker){synchronized (locker){System.out.println(i);}}}}
}

在这里插入图片描述
这个代码中,System.out.println(i);这个操作,被同一个 locker 锁了三次,到最后还是执行了这个代码,因此是个可重入锁。


1.6. 公平锁 VS 非公平锁

在这里插入图片描述
当女神和男朋友分手之后,此时应该谁上位呢?

  • 按照先来后到的顺序,谁先追女神,谁先上位,这是公平锁
  • 概率均等,这是 非公平锁

要想实现公平锁,需要付出额外的东西,比如,需要记录一下,各个线程获取锁的顺序。
synchronized 是个 非公平锁


二. 锁的特性

2.1. 锁升级

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁和重量级锁 状态。会根据情况,进行依次升级。
在这里插入图片描述
我们先解释一下什么是 偏向锁
偏向锁是 JVM 中针对 无竞争场景的锁优化机制,核心思想是:
如果一个锁被某个线程获得,后续该线程再次获取锁时无需任何同步操作(如 CAS 或 操作系统互斥)。他通过“偏向”第一次获得锁的线程,消除重复加锁的开销,适合单线程重复访问的场景。

举个例子,现在有个妹子(锁),要谈个男朋友(线程)。

  1. 有个小哥哥 (线程A),喜欢了这个妹子,有了联系方式(获取对方的id)
  2. 然后这个妹子喜欢上另一个小哥哥(线程2),那么此时直接分手(消除对方的id)
  3. 此时,分手之后,两个哥哥都想得到这个妹子,那么就要各种哄,然后强大表白(CAS自旋)。谁先令妹子心动,那么就上位,此时妹子(锁)就找到心仪的男生(线程),升级为 自旋锁
  4. 那么还有一些喜欢妹子的男生,人数挺多的,妹子该怎么做呢?如果妹子还没有找到心仪的男朋友,那么追妹子的男生太多(达到了阈值)那就全部清空;如果妹子已经有心仪的男朋友了,还有人喜欢妹子,那么直接升级为重量级锁
    在这里插入图片描述
    上述的过程就是 偏向锁的过程,以及 重偏向技术
    进行 synchronized,刚一上来,不是真加锁,而是做一个标记(获取对方的id),这个标记非常轻量,相比于加锁解锁来说,效率很高。如果没有其他线程来竞争这个锁,最终当前线程执行到解释带阿米,也就是清除上述的标记即可(不涉及到真加锁,真解锁)。如果有其他线程来竞争,就抢先一步,在另一个线程拿到锁之前,抢先拿到锁。

无锁 --> 偏向锁:代码进入 synchronized的代码块
偏向锁 --> 轻量级锁:拿到偏向锁的线程运行过程中,遇到了其他线程尝试竞争这个锁
轻量级锁 --> 重量级锁:JVM发现,当前竞争锁的情况非常激烈。就会升级到 重量级锁。

这个就是synchronized的锁变化的过程,还有就是,当前 JVM 中,只提供 锁升级 而不提供 锁降级。

2.2. 锁清除

锁清除是编译器优化的一种体现。
编译器会判定,当前这个代码逻辑是否是真的需要枷锁,如果确实不需要加锁,但是你写了 synchronized 就会自动把 synchronized 给去掉。
可能大家会有以下问题:

  1. 会出现逻辑错误,导致线程安全吗?
    是比较保守的,100%确定这个代码是单线程的时候,才会真正触发消除。
    例如下面的这个代码:
public class Demo47 {public static void main(String[] args) {StringBuffer content = new StringBuffer();content.append('a');content.append('b');content.append('c');content.append('d');System.out.println(content.toString());}
}

在这里插入图片描述
像这种单一线程中,使用 synchronized,在调用 append 的时候都会涉及到加锁和解锁,其实都没有必要,白白消耗一些资源。

  1. 如果到处都是 synchronized呢?
    意味着优化机制,只能把其中一部分,能明确判定的给优化掉。还会有很多不应该使用的,但是编译器也优化不掉。

2.3. 锁粒度

加锁和解锁之间,包含的代码越多,就认为锁的粒度就很粗。
如果包含的代码越少, 就认为锁的粒度就越细 (不是代码的行数,而是实际执行的指令 / 时间)。
一个代码中,针对细粒度的代码枷锁,就可能被优化成粗粒度的加锁。
在这里插入图片描述
上面黑色的是细粒度的写法,红色的是粗粒度的写法。本来是执行多次加锁解锁的,优化成一次加锁解锁、每次解锁之后,重新加锁,都会增加竞争。
锁是越粗越优秀吗?
还是需要具体问题具体分析的
锁粗了,就会影响线程的并发操作。
例如: 确实有三个线程,每一个都要加锁,粗化成一把锁,合理的
如果是有三个线程,其中两个线程需要加锁,一个线程不需要,粗化成一把锁使得本来不需要加锁的
能够并行执行的事情,也变成串行,那肯定是不可能的


下个博客我们讲解 CAS 和一些多线程常用的类,我们不见不散!

http://www.dtcms.com/a/342195.html

相关文章:

  • 大模型自我进化框架SE-Agent:开启软件工程自动化新时代
  • Confluent 实时代理:基于 Kafka 流数据的创新实践
  • git 常用命令整理
  • 拂去尘埃,静待花开:科技之笔,勾勒城市新生
  • Linux基础(1) Linux基本指令(二)
  • 大模型推理并行
  • 机器学习7
  • 以往内容梳理--HRD与MRD
  • 《深入探索 Java IO 流进阶:缓冲流、转换流、序列化与工具类引言》
  • 事件驱动流程链——EPC
  • Metrics1:Intersection over union交并比
  • tail -f与less的区别
  • Python Excel 通用筛选函数
  • 【C++】模板(进阶)
  • Rancher 管理的 K8S 集群中部署常见应用(MySQL、Redis、RabbitMQ)并支持扩缩容的操作
  • ubuntu编译ijkplayer版本k0.8.8(ffmpeg4.0)
  • Spring Boot整合Amazon SNS实战:邮件订阅通知系统开发
  • 将windows 的路径挂载到Ubuntu上进行直接访问
  • C++---辗转相除法
  • VB.NET发送邮件给OUTLOOK.COM的用户,用OUTLOOK.COM邮箱账号登录给别人发邮件
  • Azure的迁移专业服务是怎么提供的
  • 带有 Angular V14 的 Highcharts
  • Transformer在文本、图像和点云数据中的应用——经典工作梳理
  • 【解决方案系列】大规模三维城市场景Web端展示方案
  • C++STL-stack和queue的使用及底层实现
  • 阿里云搭建flask服务器
  • 2021年ASOC SCI2区TOP,改进遗传算法+自主无人机目标覆盖路径规划,深度解析+性能实测
  • Java 16 新特性及具体应用
  • Redis 奇葩问题
  • Python break/continue