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 获取到 锁A
线程2 获取到 锁B
接下来,1尝试获取B,2尝试获取A。
就同样出现死锁了!!!举例:
(“互不相让,不懂合作,僵持不前”,执行完run,线程才是结束,这里是僵持住们无法结束了)
运行这个代码,打开jconsole进行查看:
(上边这个例子中,如果约定加锁顺序,先对A加锁,后对B加锁 ~ ~此时,死锁仍然可以解决 ~ ~) -
N个线程M把锁
哲学家就餐问题
(注意,上图中,每个滑稽,都只能拿挨着他的两只筷子)
以上,描述完“哲学家就餐问题”(吃面条——去CPU上运行;思考人生——放下CPU被调度走;拿筷子——加锁)
要想解决死锁问题,就要能够了解原因
↓↓↓
【产生死锁的四个必要条件 ~ ~】
-
互斥使用。(最基本的特性,不太好破坏)
获取锁的过程是互斥的。一个线程拿到了这把锁,另一个线程也想获取,就需要阻塞等待。 -
不可抢占。(锁最基本的特性,不太好破坏)
一个线程拿到了锁之后,只能主动解锁,不能让别的线程强行把锁抢走 ~ ~ -
请求保持。(代码结构,不一定能破坏,要看实际需求 ~有时候代码就是需要两个锁都拿到)
一个线程拿到了锁A之后,在(一直)持有A(没有释放)的前提下,(总是)尝试获取B。 -
循环等待/环路等待(代码结构相关,最容易破坏 ~ ~只需要制定一定的规则,就可以有效的避免循环等待!!!比如:指定加锁顺序 ~ ~)
解决死锁问题,核心思路:破坏上述的必要条件之一,就搞定!!!
【解决死锁】从原因入手(第四条,最容易突破,有很多种方案 ~ ~)
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)前边说过的:
上述代码,编译器发现,每次循环都要读取内存,开销太大。于是就把读取内存操作优化成读取寄存器操作,提高效率。/
/(2)JMM(Java Memory Model)模型(一个抽象的 概念)
上述代码,编译器发现,每次循环都要读取“主内存”,就会把数据从“主内存”中复制到“工作内存”中,后续每次都是读取“工作内存”/
工作内存——不是真正的内存,而是CPU寄存器 或者 CPU的缓存(L1,L2,L3,三级缓存),称为“工作存储区”。
主内存——也就是内存。
、、
引入“工作内存”这个概念,而不直接说“CPU寄存器”,主要是为了“跨平台”。并且不用像说“CPU寄存器或缓存中”这样拗口。 -
禁止指令重排序
注意!!!
只有锁可以完全解决线程安全问题,而 Java 中的锁有两种:synchronized 和 Lock。
volatile 可以解决内存可见性问题,但不能完全解决线程安全问题。
sleep不能解决线程安全问题。
相关文章:

JavaEE初阶——多线程(线程安全-锁)
复习上节内容(部分-掌握程度不够的) 加锁,解决线程安全问题。 synchronized关键字,对锁对象进行加锁。 锁对象,可以是随便一个Object对象(或者其子类的对象),需要关注的是ÿ…...

Stable Diffusion 提示词语法
1.提示词基础 1.提示词之间用英文逗号,分隔 2.提示词之间是可以换行的 3.权重默认为1,越靠前权重越高 4.数量控制在75个单位以内 2.提示词各种符号的意义 2.1 ()、[]、{}符号 权重值()小括号[]中括号{}大括号默认1111层()1.1[]0.9{}1.052层(()) 1.121.21[[]]0.920.81{{}}1.…...

【功能安全】安全确认
目录 01 功能安全确认介绍 02 安全确认用例 03 安全确认模板 01 功能安全确认介绍 定义: 来源...

在pycharm2024.3.1中配置anaconda3-2024-06环境
version: anaconda3-2024.06-1 pycharm-community-2024.3.1 1、安装anaconda和pycharm 最新版最详细Anaconda新手安装配置环境创建教程_anaconda配置-CSDN博客 【2024最新版】超详细Pycharm安装保姆级教程,Pycharm环境配置和使用指南,看完这一篇就够了…...

linux不同发行版中的主要差异
一、初始化系统 Linux不同发行版中的系统初始化系统(如 System V init、Upstart 或 systemd) System V init: 历史:System V init 是最传统的 Linux 系统初始化系统,起源于 Unix System V 操作系统。运行级别ÿ…...

概率论得学习和整理29: 用EXCEL 描述二项分布
目录 1 关于二项分布的基本内容 2 二项分布的概率 2.1 核心要素 2.2 成功K次的概率,二项分布公式 2.3 期望和方差 2.4 具体试验 2.5 概率质量函数pmf 和cdf 3 二项分布的pmf图的改进 3.1 改进折线图 3.2 如何生成这种竖线图呢 4 不同的二项分布 4.1 p0.…...

C++打造局域网聊天室第九课: 客户端队列及其处理线程
文章目录 前言一、添加客户端队列的参数初始化二、相关函数总结 前言 C打造局域网聊天室第九课: 客户端队列及其处理线程 一、添加客户端队列的参数初始化 在Server.cpp的 ListenThreadFunc()函数内的其他操作处实现客户端队列的添加。 首先进行部分参数的初始化…...

请求go web后端接口 java安卓端播放视频
前端代码 添加gradle依赖 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 添加访问网络权限 <uses-permission android:name"android.permission.INTERNET" />允许http 请求请求 andro…...

XML Schema 复合类型 - 混合内容
XML Schema 复合类型 - 混合内容 XML Schema 是一种用于定义 XML 文档结构和内容的语言。在 XML Schema 中,复合类型是一种包含其他元素和/或属性的复杂类型。混合内容(Mixed Content)是复合类型的一种特殊形式,它允许元素包含其…...

第8章 搬移特性
8.1 搬移函数 模块化是优秀软件设计的核心所在,好的模块化能够让我在修改程序时只需理解程序的一小部分。为了设计出高度模块化的程序,我得保证互相关联的软件要素都能集中到一块,并确保块与块之间的联系易于查找、直观易懂。同时,…...

ARM/Linux嵌入式面经(五九):海尔
1.以后打算在哪里工作 问题回答: 1. 以后打算在哪里工作? 回答这个问题时,我首先会考虑我的个人目标、职业规划以及家庭和生活因素。从职业发展的角度来看,我希望能够在技术氛围浓厚、创新能力强、且能提供良好职业成长机会的地方工作。具体来说,我对以下几个方向特别感…...

java中的List、数组和set
在Java中,List、数组(Array)和Set 是三种常用的数据结构,它们各自有不同的特性、用途和实现方式。下面我们将深入探讨这三者的特点、区别以及它们在 Java 中的常见使用场景。 1. 数组(Array) 特性&#x…...

freeswitch(配置文件结构)
vars.xml<X-PRE-PROCESS cmd="set" data="default_password=1234"/>default_password:设置默认密码,为安全起见,建议在部署后立即更改此密码,以防止盗用。<X-PRE-PROCESS cmd=<...

ARMS 用户体验监控正式发布原生鸿蒙应用 SDK
作者:羿莉 背景 对企业数据进行敏感数据扫描和保护可以提升企业或组织的数据安全。一方面敏感数据可能包括个人身份信息、财务记录、医疗记录等,定期扫描这些数据可以防止未经授权的访问和泄露。 另一方面,许多国家和地区都有关于数据保护的…...

使用 esrally race 测试 Elasticsearch 性能:实践指南
在 Elasticsearch 性能优化和容量规划中,使用 esrally 进行基准测试是官方推荐的方式。通过 esrally race 命令,您可以针对不同的数据集与挑战类型,对 Elasticsearch 集群进行精确的性能评估。本文将简要介绍常用的数据集与挑战类型ÿ…...

OkHttp源码分析:分发器任务调配,拦截器责任链设计,连接池socket复用
目录 一,分发器和拦截器 二,分发器处理异步请求 1.分发器处理入口 2.分发器工作流程 3.分发器中的线程池设计 三,分发器处理同步请求 四,拦截器处理请求 1.责任链设计模式 2.拦截器工作原理 3.OkHttp五大拦截器 一&#…...

中国计算机学会计算机视觉专委会携手合合信息举办企业交流活动,为AI安全治理打开“新思路”
近期,《咬文嚼字》杂志发布了2024年度十大流行语,“智能向善”位列其中,过去一年时间里,深度伪造、AI诈骗等话题屡次登上热搜,AI技术“野蛮生长”引发公众担忧。今年9月,全国网络安全标准化技术委员会发布了…...

重生之我在异世界学编程之C语言:深入预处理篇(上)
大家好,这里是小编的博客频道 小编的博客:就爱学编程 很高兴在CSDN这个大家庭与大家相识,希望能在这里与大家共同进步,共同收获更好的自己!!! 本文目录 引言正文一、预处理的作用与流程…...

dolphinscheduler服务RPC框架源码解析(二)RPC核心注解@RpcService和@RpcMethod设计实现
1.工程目录 从3.2.1版本之后这个dolphinscheduler中的RPC框架工程就从原来的dolphinscheduler-remote工程重构到了dolphinscheduler-extract工程。 dolphinscheduler 父项目 dolphinscheduler-extract RPC服务项目 dolphinscheduler-extract-alert 监控告警服务RPC接口定义、…...

【从零开始入门unity游戏开发之——C#篇04】栈(Stack)和堆(Heap),值类型和引用类型,以及特殊的引用类型string
文章目录 知识回顾一、栈(Stack)和堆(Heap)1、什么是栈和堆2、为什么要分栈和堆3、栈和堆的区别栈堆 4、总结 二、值类型和引用类型1、那么值类型和引用类型到底有什么区别呢?值类型引用类型 2、总结 三、特殊的引用类…...

ARCGIS国土超级工具集1.2更新说明
ARCGIS国土超级工具集V1.2版本,功能已增加至47 个。在V1.1的基础上修复了若干使用时发现的BUG,新增了"矢量分割工具"菜单,同时增加及更新了了若干功能,新工具使用说明如下: 一、勘测定界工具栏更新界址点成果…...

暂停window11自动更新
window11 的自动更新功能,一方面在后台占用资源,容易导致电脑卡顿;另一方面,“更新并关机” 和 “更新并重启” 的设置令人极其反感。很多补丁兼容性很差,更新后极易引发电脑蓝屏、闪屏等意想不到的 bug。 1.winR打开运…...

Git简介和特点
目录 一、Git简介 二、Git特点 1.集中式和分布式 (1)集中式版本控制系统 (2)分布式版本控制系统 2.版本存储方式的差异 (1)直接记录快照,而非差异比较 3.近乎所有操作都是本地执行 一、Git简介 Git是目前世界上最先进的的分布式控制系统(没有之一…...

如何通过docker 部署minio,端口号为9105
通过Docker部署MinIO对象存储服务,并指定API端口为9105,可以按照以下步骤进行。我们将基于已有的资料来详细说明这一过程。 1. 准备工作 首先,确保你的系统上已经安装了Docker。如果没有安装,可以根据官方文档指导完成安装。接下…...

设置Qt程序开机自启动(windows版本)
前言 本文展示在windows环境下,通过代码实现更改系统注册表的方式来实现程序的开机自动启动。 一、注册表 需要更改的系统注册表为: HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run 二、代码演示 1.头文件 头文件autorun.h #ifndef …...

【HarmonyOS】鸿蒙获取appIdentifier,Identifier
【HarmonyOS】鸿蒙获取appIdentifier,Identifier 一、前言 三方后台需要填写的所谓appIdentifier,Identifier信息,其实对应鸿蒙应用的appID。 二、解决方案: 注意,模拟器获取data.signatureInfo.appIndentifer为空…...

【Rust自学】3.5. 控制流:if else
3.5.0. 写在正文之前 欢迎来到Rust自学的第三章,一共有6个小节,分别是: 变量与可变性数据类型:标量类型数据类型:复合类型函数和注释控制流:if else(本文)控制流:循环 通过第二章…...

美国信息学奥林匹克竞赛USACO 2024年12月比赛铜级问题1. 循环舍入-答案代码
题目见: USACO 2024年12月比赛铜级问题1. 循环舍入(USACO 2024 December Contest, Bronze Problem 1. Roundabount Rounding) 最简单的青铜级 亲测所有得分点通过哈! 下一篇会给解题分析哦! #include <iostream> using namespace std;…...

Llama3模型详解 - Meta最新开源大模型全面解析
📚 Meta最新发布的Llama3模型在开源社区引起广泛关注。本文将全面解析Llama3的技术特点、部署要求和应用场景。 一、模型概述 1. 基本信息 发布机构: Meta AI开源协议: Llama 2 Community License Agreement模型规格: 7B/13B/34B/70B训练数据: 2万亿tokens上下文长…...

2021-02-12 c++里面cin.sync()函数的意思
回复急~救救菜鸡吧,C用cin.clear()和cin.sycn()清空缓存区一直清不了!_编程语言-CSDN问答 标识符号为:goodbit 无错误 Eofbit 已到达文件尾 failbit 非致命的输入/输出错误可挽回 badbit 致命的输入/输出错误无法挽回 int a 0;cin >> a;if (cin.rdstate() i…...