JavaEE初阶——多线程(线程安全-锁)

复习上节内容(部分-掌握程度不够的)

加锁,解决线程安全问题。
synchronized关键字,对锁对象进行加锁。
锁对象,可以是随便一个Object对象(或者其子类的对象),需要关注的是:是否是对同一个锁对象进行加锁(锁竞争)

进入代码块,加锁;
离开代码块,解锁。

synchronized修饰普通方法,相当于给this加锁(锁对象this);
synchronized修饰静态方法,相当于给类对象加锁


从导致线程安全问题的原因,进行解决。

synchronized(续 上一篇)

synchronized特性。

  • synchronized加锁效果具有互斥性
  • 可重入
    拿到锁的线程再次对该锁对象进行加锁,不会阻塞;
    [ 代码示例 ]
Thread t = new Thread(()->{synchronized(locker){synchronized(locker){System.out.println("hello");}}
});

解释:
(1)上述代码,可以正常打印hello。
(2)原因:
这两次加锁,是在同一个线程进行的。
当前由于是同一个线程,此时锁对象,就知道了第二次加锁的线程,就是持有锁的线程。第二次操作,就可以直接放行通过,不会出现阻塞。 ——可重入(所以这个代码并不会出现锁冲突)
(3)好处:
a.可以避免上述代码出现死锁的情况。(Java中的锁,都是可重入锁。)
如果没有这个特性——比如C++,用的std::mutex锁,就是不可重入的,一旦以上代码出现阻塞,无法自动恢复,所以这个线程就卡死了 ~ ~(这里出现的卡死,就称为“死锁”)
b.其他容易出现这种死锁的情况:
方法/函数的调用关系复杂,加锁的代码比较隐蔽。如下例:

void func1(){加锁func2();解锁;
}void func2(){func3();
}void func3(){func4();
}void func4(){加锁;……解锁;
}

以上示例的代码,直观上看,每个地方都是只加了一次锁。
但是由于复杂的调用关系,就可能导致,加锁重复了。

(4)注意:双重加锁,本身就是代码写的有问题(是有问题的代码逻辑,本来就不应该这样写),所以也没有应用场景 ~ ~
可重入这个特性,就是为了防止咱们在“不小心”中引入问题,就算你不小心了,也没事!!!(即:写错了也能正常运行)

(5)原理:如何能实现可重入性

可重入锁:

内部持有两个信息
a.当前这个锁是被哪个线程持有的
b.加锁次数计数器

初始值为0,加锁一次,+1一次,第一次加锁——也就是为0的时候加锁,会同时记录线程是谁;
判定当前加锁线程是否上持有锁的线程,如果不是同一个线程,阻塞;如果是同一个线程,就只是让计数器++,即可 ~ 没有别的操作了 ~ ~

注意
a.synchronized嵌套多层也可以保证在正确的时机解锁
b.计数器是真正用来识别解锁时机的关键要点 ~ ~
这个源码在JVM中,C++代码实现出来的,在idea中是看不到的

【源码】:
Java标准库的源码:这个是通过Java代码写的,idea中都能看到,虽然是.class的,但是idea上你看到的是idea自动帮你反编译的
JVM里的源码:C++写的,在Java层看不到,需要额外下载jvm的源码来看 ~ ~


c.最外层的{进行加锁,最外层的}进行解锁
一共只有一把锁(一个锁对象,只有一把锁)
d.锁的加锁次数和线程,不能通过函数进行获取(由JVM封装好了,我们知道就行,不必去干预)
e.(接d.)jconsole可以查看到的是线程的状态,能一定程度上反应出锁的状态,但是并不能获取锁的这两个信息(属性)

PS:【计数器】,这种处理方式,很多地方都会使用到,可以理解为一种处理技巧。


死锁

——多线程代码中的一类经典问题 ~ ~ 也是经典面试题
(加锁是可以解决线程安全问题,但是如果加锁方式不当,就可能产生死锁。)

死锁,属于程序中最严重的一类bug。一旦出现死锁,线程就“卡住了”,无法继续工作。(所以,要想办法避免 ~ ~)

【死锁的三种典型场景】

  1. 一个线程,一把锁
    刚才的代码中,如果是不可重入锁,并且只有一个线程对这把锁加锁两次,就会出现死锁。

  2. 两个线程,两把锁
    线程1 获取到 锁A
    线程2 获取到 锁B
    接下来,1尝试获取B,2尝试获取A。
    就同样出现死锁了!!!

    举例:
    (“互不相让,不懂合作,僵持不前”,执行完run,线程才是结束,这里是僵持住们无法结束了)在这里插入图片描述
    运行这个代码,打开jconsole进行查看:
    在这里插入图片描述
    在这里插入图片描述
    (上边这个例子中,如果约定加锁顺序,先对A加锁,后对B加锁 ~ ~此时,死锁仍然可以解决 ~ ~)

  3. N个线程M把锁
    哲学家就餐问题
    在这里插入图片描述
    (注意,上图中,每个滑稽,都只能拿挨着他的两只筷子)
    在这里插入图片描述
    以上,描述完“哲学家就餐问题”(吃面条——去CPU上运行;思考人生——放下CPU被调度走;拿筷子——加锁)

要想解决死锁问题,就要能够了解原因
↓↓↓
【产生死锁的四个必要条件 ~ ~】

  1. 互斥使用。(最基本的特性,不太好破坏)
    获取锁的过程是互斥的。一个线程拿到了这把锁,另一个线程也想获取,就需要阻塞等待。

  2. 不可抢占。(锁最基本的特性,不太好破坏)
    一个线程拿到了锁之后,只能主动解锁,不能让别的线程强行把锁抢走 ~ ~

  3. 请求保持。(代码结构,不一定能破坏,要看实际需求 ~有时候代码就是需要两个锁都拿到)
    一个线程拿到了锁A之后,在(一直)持有A(没有释放)的前提下,(总是)尝试获取B。

  4. 循环等待/环路等待(代码结构相关,最容易破坏 ~ ~只需要制定一定的规则,就可以有效的避免循环等待!!!比如:指定加锁顺序 ~ ~)

解决死锁问题,核心思路:破坏上述的必要条件之一,就搞定!!!

【解决死锁】从原因入手(第四条,最容易突破,有很多种方案 ~ ~)
1)引入额外的筷子
2)去掉一个线程
3)引入计数器,限制最多同时多少个人同时吃面
==》1)2)3),这三个方案虽然不复杂,但是,普适性不高,有的时候用不了 ~ ~

4)引入加锁顺序的规则(普适性高,方案容易落地)
5)“银行家算法”
能解决死锁问题,但是这个方案太复杂了!!!理论可行,实践中并不推荐。实际开发中千万不要这么做。先不谈解决死锁问题,很可能你写的银行家算法本身就存在bug。

【问题】“可不可以给‘哲学家’编号,反正每次只能有一位哲学家吃,让他们按编号用餐?”
答:不行。
线程调度的大前提是“随机调度”。
想办法让某个线程 先加锁,违背了“随机调度”根本原则。可行性是不高的。
(而约定加锁顺序,在写代码的层面上,是非常容易做到的 ~ ~)


Java标准库中的线程安全类

标准库有很多 集合类

在这里插入图片描述——这些类,都线程不安全。
多个线程,尝试修改同一个上述的对象,就很容易出现问题!!!
(注意:这里“很容易出现问题”,而不是100%,也可能这个代码写出来后,是没有问题的,具体代码具体分析。多线程的代码,稍微变化一点,就可能有不一样的结果)

在这里插入图片描述
这几个类,自带锁了 ~ ~
在多线程环境下的时候,能好点儿 ~ ~
但是,也不是100%不出问题!!只是概率比上面小很多。具体代码,具体分析!!!
(多线程的代码,稍微变化一点,就可能有不一样的结果)
注意:这几个类,都是标准库即将弃用的 ~ ~现在暂时还保留着,未来某一天新版本的jdk可能就把这些内容删了 ~ ~所以,在写新的代码的时候,就尽量别用了,不推荐 ~ ~

拓展:【jdk版本升级】
之前用的一直是jdk8这个经典版本,2014年发行。之后要学到Spring,Spring升级到3之后,不支持jdk8了,最低也需要jdk17.
(另外,Spring升级了,旧版本的Spring虽然仍然能使用,但是修改起来非常麻烦,所以还是建议采用用jdk17这种方案。)
jdk的版本升级虽然快,但是新版本的新东西不算多 ~ ~
【更改方法】
下载安装jdk17,然后把idea设置一下,使用jdk17即可 ~ ~(并不费事,不要退缩!!!)

继续讲解引起线程安全问题的原因

内存可见性

如果一个线程写,一个线程读,也是可能有线程安全问题的。

代码示例:

//这个代码中,预期通过t2线程输入的整数,只要输入的不为0,就可以使t1线程结束。public static int flag = 0;//public 类中的成员变量
public static void main(){Thread t1 = new Thread(()->{while(flag == 0){//循环体里,啥都不写}System.out.println("t1 线程结束!");});Thread t2 = new Thread(()->{System.out.println("请输入 flag 的值:");Scanner sc = new Scanner(System.in);flag = sc.nestInt();});t1.start();t2.start();
}

【预期】通过t2线程输入的整数,只要输入的不为0,就可以使t1线程结束。

【问题】实际输入非0的时候,发现t1并没有真的结束!!!

以上代码出现问题的原因:
(1)JVM对代码做出了优化。
t1的循环体内,什么都没有写,核心指令就只有两条:a.load读取内存中flag的值,到CPU寄存器里。b.拿着寄存器的值和0进行比较(条件跳转指令 ~ ~)

	所以,上述循环执行速度就会非常快!!!(a、b两条指令,快速、反复的执行)这样,在这个执行过程中,有两个关键要点:①load操作执行的结果,每次都是一样的!!!(想要输入,也是过几秒才能输入,人并没有那么快。在这几秒之内,已经执行了不知道多少次循环,上百亿次 ~ ~)②load操作,它的开销远远超过条件跳转!!访问寄存器的速度,远远超过访问内存!!频繁执行load和条件跳转,load的开销大,并且load的结果又没有变化(真正出现变是好几秒之后的事:用户输入)。此时,JVM就产生怀疑:这里的load操作是否真的有存在的必要???——JVM就可能做出代码优化 ~ ~JVM把上述load操作,给优化掉!!!(只有前几次执行load,后续发现,load反正都一样,静态分析代码,也没看到哪里改了flag,因此,就直接激进的把load操作,给干掉了)load操作被干掉之后,就相当于不再重复读内存,而直接使用寄存器中之前“缓存”的值 ~ ~大幅地提高了循环的执行速度!!!不过,也因此,导致t2修改了flag的内容,但是t1没有看到这个内存的变化**==》内存可见性问题**

(2)t2修改了内存,但是t1没有看到这个内存的变化(所谓:内存可见性)

以上两条原因,(1)导致了(2),进而导致程序出现了问题。

【拓展:JVM代码优化功能】
(其他)编译器/JVM,都非常厉害。

很多地方都会涉及到代码优化。
确实存在有些程序猿代码写的不太行。因此,设计JVM和编译器的大佬就引入这样的优化能力,在优化的加持下,就能让你即使写不出太高效的代码,最终的执行效率也不会太差 ~ ~ ~

有没有优化,差别非常大。(比如:有服务器,开启优化,10分钟完成启动;关闭优化,30min+)

虽然我们写的只是一份代码,但是编译器和JVM就能只能分析出:当前这份代码哪里不太合理,然后对代码进行调整 ~ ~保证了,在原有逻辑不变的前提下,提高程序效率 ~ ~
很多主流语言的编译器,都有这样的能力(对代码进行不合理分析,调整,逻辑不变,效率提升)
但是!!!原有逻辑不变这点,编译器是没有那么容易正确保持的。(单线程下,还好;多线程下,很容易出现误判——这个可以视作bug)

【对“多线程代码,稍微变化一点,就可能有不一样的结果”的一点例子,帮助理解】

//其实就是对刚才的代码略加改动:在循环体中,加入sleep语句;
Thread t1 = new Thread(()->{while(flag==0){try{Thread.sleep(10);//不加sleep,一秒钟循环上百亿次//load操作的开销非常大,所以优化的迫切程度就更高//加了sleep,一秒钟循环1000次//load整体开销就没那么大了,优化的迫切程度就降低了。//所以可知:编译器什么时候触发优化,不一定。进而,什么时候出现“内存可见性问题”,也就不一定了。(代码稍微改动一点,结果就截然不同。)}catch(InterruptedException e){e.printStackTrace();}}System.out.println("t1线程结束!");
});

解决内存可见性问题【volatile关键字】

——由上述文字可知:需要解决“是否选择启用优化”。

【volatile】强制关闭优化/或称:强制读取内存。
(可以确保示例代码中,每次循环都会重新从内存中读取数据)

这样做,开销是大了,效率是低了,但是数据的准确性、逻辑的正确性,都提高了。
(更多时候,快没有准重要,就加volatile;确实有时候需要快而不要求准,就不加volatile。根据场景需求,作取舍)

这样volatile关键字,就把是否启用优化 的 选择权 ,交给了程序猿自己。

【volatile功能】

  1. 保证内存可见性(核心功能之一)
    /关于 内存可见性,有两种表述/
    /(1)前边说过的:
    上述代码,编译器发现,每次循环都要读取内存,开销太大。于是就把读取内存操作优化成读取寄存器操作,提高效率。
    /
    /(2)JMM(Java Memory Model)模型(一个抽象的 概念)
    上述代码,编译器发现,每次循环都要读取“主内存”,就会把数据从“主内存”中复制到“工作内存”中,后续每次都是读取“工作内存”
    /
    工作内存——不是真正的内存,而是CPU寄存器 或者 CPU的缓存(L1,L2,L3,三级缓存),称为“工作存储区”。
    主内存——也就是内存。
    、、
    引入“工作内存”这个概念,而不直接说“CPU寄存器”,主要是为了“跨平台”。并且不用像说“CPU寄存器或缓存中”这样拗口。

  2. 禁止指令重排序


注意!!!

只有锁可以完全解决线程安全问题,而 Java 中的锁有两种:synchronized 和 Lock。

volatile 可以解决内存可见性问题,但不能完全解决线程安全问题。

sleep不能解决线程安全问题。

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

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

相关文章

如何在NGINX中实现基于IP的访问控制(IP黑白名单)?

大家好,我是锋哥。今天分享关于【如何在NGINX中实现基于IP的访问控制(IP黑白名单)?】面试题。希望对大家有帮助; 如何在NGINX中实现基于IP的访问控制(IP黑白名单)? 1000道 互联网大…

Docker--Docker Registry(镜像仓库)

什么是Docker Registry? 镜像仓库(Docker Registry)是Docker生态系统中用于存储、管理和分发Docker镜像的关键组件。 镜像仓库主要负责存储Docker镜像,这些镜像包含了应用程序及其相关的依赖项和配置,是构建和运行Doc…

微信小程序:实现节点进度条的效果;正在完成的节点有动态循环效果;横向,纵向排列

参考说明 微信小程序实现流程进度功能 - 知乎 上面的为一个节点进度条的例子&#xff0c;但并不完整&#xff0c;根据上述代码&#xff0c;进行修改完善&#xff0c;实现其效果 横向效果 代码 wxml <view classorder_process><view classprocess_wrap wx:for&quo…

window下的qt5.14.2配置vs2022

这里做一个笔记&#xff0c;已知qt5.14.2和vs2022不兼容&#xff0c;无法自动扫描到vs的编译器。但由于团队协作原因&#xff0c;必须使用qt5.14.2&#xff0c;并且第三方库又依赖vs2022。其实qt5.15.2是支持vs2022的&#xff0c;如果能够用qt5.15.2&#xff0c;还是建议使用qt…

Java-27 深入浅出 Spring - 实现简易Ioc-03 在上节的业务下手动实现IoC

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 大数据篇正在更新&#xff01;https://blog.csdn.net/w776341482/category_12713819.html 目前已经更新到了&#xff1a; MyBatis&#xff…

梳理你的思路(从OOP到架构设计)_UML应用:业务内涵的分析抽象表达01

目录 1、 系统分析(System Analysis) 系統分析的涵意 业务(领域)知识 业务内涵 业务(领域)概念 2、举例(一) &#xff1a;东方传说 UML与建模工具 1、 系统分析(System Analysis) 系統分析的涵意 许多人在学习系统分析(System Analysis)时&#xff0c;常迷失于其字面上…

Web 安全 跨站 跨域 XSS CSRF

跨站 跨站即 cross-site&#xff0c;它和同站&#xff08;same-site&#xff09;相对&#xff0c;对协议和端口号无要求&#xff0c;只要两个 URL 的 eTLD 1 一致&#xff0c;就能称为同站。那么什么是 eTLD 呢&#xff1f; eTLD 即 effective top level domain&#xff0c;…

k8s服务搭建与实战案例

Kubernetes&#xff08;K8s&#xff09;作为一个开源的容器编排平台&#xff0c;广泛应用于现代的云原生应用架构中。以下是一些常见的 **Kubernetes 实战案例**&#xff0c;包括从基础部署到高级应用场景的使用。通过这些案例&#xff0c;可以更好地理解 K8s 的运作原理和最佳…

PYQT5程序框架

pyqt5程序框架_哔哩哔哩_bilibili 1.UI代码 Qhkuja.py # -*- coding: utf-8 -*-# Form implementation generated from reading ui file Qhkuja.ui # # Created by: PyQt5 UI code generator 5.15.7 # # WARNING: Any manual changes made to this file will be lost when py…

基于MobileNet v2模型的口罩实时检测系统实现

基于kaggle数据集训练的模型其实现结果如下&#xff1a; 代码结构如下&#xff1a; 实时口罩检测器&#xff1a; 从导航栏中的链接“实时的口罩检测器”功能&#xff0c;该系统包含一个实时检测用户是否佩戴口罩的功能。基于图片的口罩检测器&#xff1a; 从另一个导航链接“基…

高效项目托管指南:从本地到 GitHub 的完整流程

在现代软件开发中&#xff0c;将项目托管在 GitHub 上是一个常见且高效的方式。GitHub 不仅可以用作版本控制工具&#xff0c;还能帮助你与团队协作或展示自己的项目。本文将带你一步步完成项目的打包和上传。 高效项目托管指南&#xff1a;从本地到 GitHub 的完整流程 1. 准备…

用TPS54531绘制BUCK电路板

首先&#xff0c;这TPS54531 是一款非同步降压转换器芯片&#xff08;异步&#xff09;。 这是BUCK的基本原理图&#xff0c;它是异步的。 我们用这款芯片来控制MOS管的高频开关&#xff0c;以此实现降压。 这里使用的应该是CCM模式。 这里&#xff1a; Vi为24V&#xff0c;…

【新人系列】Python 入门(十六):正则表达式

✍ 个人博客&#xff1a;https://blog.csdn.net/Newin2020?typeblog &#x1f4dd; 专栏地址&#xff1a;https://blog.csdn.net/newin2020/category_12801353.html &#x1f4e3; 专栏定位&#xff1a;为 0 基础刚入门 Python 的小伙伴提供详细的讲解&#xff0c;也欢迎大佬们…

人工智能增强的音频和聊天协作服务

论文标题&#xff1a;AI-enabled Audio and Chat Collaboration Services 中文标题&#xff1a;人工智能增强的音频和聊天协作服务 作者信息&#xff1a; Emil P. Andersen, Norwegian Defence Research Establishment (FFI), Kjeller, NorwayJesper R. Goksr, Sindre E. Ha…

【原创】- 澳门预约医疗系统 - 一个基于Vue3编写的程序

在空余时间写了一个前端预约医疗系统&#xff0c;用Vue3写的一套前端模块&#xff0c;里面数据都是本地模拟&#xff0c;一个练手的简单项目。 此项目主要功能有&#xff1a; 1.预约挂号、挂号记录 2.疫苗接种 3.就医记录 4.科室导航 5.AI问诊 6.个人病例 7.支付可配置化 8.健康…

Cadence学习笔记 3 MCU主控原理图绘制

基于Cadence 17.4&#xff0c;四层板4路HDMI电路 更多Cadence学习笔记&#xff1a;Cadence学习笔记 1 原理图库绘制Cadence学习笔记 2 PCB封装绘制 目录 3、MCU主控原理图绘制 快捷键总结&#xff1a; 3、MCU主控原理图绘制 新建原理图Design&#xff0c;选择好SCH文件夹&…

vue2实现答题组件

需求 实现一个答题组件&#xff0c;点击正确的选项&#xff0c;该选项背景变绿色&#xff1b;点击错误的选项&#xff0c;该选项背景变红色。不管点击了什么选项&#xff0c;延迟一秒后切换下一题。 每次出题&#xff0c;从题库中选随机选择一道用户此次进入这个页面后还没有…

3D相框案例讲解(详细)

前言 通过现阶段的学习&#xff0c;我们已经掌握了HTML&#xff0c;CSS和JS部分的相关知识点&#xff0c;现在让我们通过一篇案例&#xff0c;来巩固我们近期所学的知识点。 详细视频讲解戳这里 任务一 了解目标案例样式 1.1了解案例 3D相框 1.2 分析案例 首先我们看到一个…

SuperMap GIS基础产品FAQ集锦(20241216)

一、SuperMap iDesktopX 问题1&#xff1a;想问一下 SuperMap iDesktopX可以修改倾斜入库后数据的位置吗 11.2.1 【解决办法】在配置文件&#xff08;*.scp&#xff09;中找打position属性&#xff0c;修改其中的xyz值&#xff0c;用于改变位置&#xff1b;修改后如果想要相机…

2.11.静态链表

一.静态链表的基本概念&#xff1a; 1.上图说明&#xff1a;索引为0处是头结点&#xff0c;头结点不存储数据&#xff0c;但存储下一个结点的数组下标&#xff0c;本例中头结点里存储的下一个结点的数组下标为2&#xff0c;即索引为2的结点为头结点后的第一个结点&#xff0c;以…