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不能解决线程安全问题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

day2 数据结构 结构体的应用

思维导图 小练习: 定义一个数组,用来存放从终端输入的5个学生的信息【学生的信息包含学生的姓名、年纪、性别、成绩】 1>封装函数 录入5个学生信息 2>封装函数 显示学生信息 3>封装函数 删除第几个学生信息,删除后调用显示学…

五、网络层:控制平面,《计算机网络(自顶向下方法 第7版,James F.Kurose,Keith W.Ross)》

目录 一、导论 二、路由选择算法 2.1 路由(route)的概念 2.2 网络的图抽象 2.2.1 边和路由的代价 2.2.2 最优化原则 2.3 路由的原则 2.4 路由选择算法的分类 2.5 link state 算法 2.5.1 LS路由工作过程 2.5.2 链路状态路由选择(lin…

内网是如何访问到互联网(H3C源NAT)

H3C设备NAPT配置 直接打开29篇的拓扑,之前都配置好了 「模拟器、工具合集」复制整段内容 链接:https://docs.qq.com/sheet/DV0xxTmFDRFVoY1dQ?tab7ulgil 现在是出口路由器可以直接访问61.128.1.1,下面的终端访问不了,需要做NAPT源…

生产者-消费者模型

目录 生产者-消费者模型介绍 生产者-消费者模型优点 生产者-消费者之间的关系 基于阻塞队列实现生产者-消费者模型 基于环形队列实现生产者-消费者模型 生产者-消费者模型介绍 ● 计算机中的生产者和消费者本质都是线程/进程 ● 生产者和消费者不直接通讯,而是…

.NET6 WebAPI从基础到进阶--朝夕教育

1、环境准备 1. Visual Studio 2022 2. .NET6 平台支持 3. Internet Information Services 服务器( IIS ) 4. Linux 服务器 【 CentOS 系统】 ( 跨平台部署使用 ) 5. Linux 服务器下的 Docker 容器( Docker 部署使用) …

Linux系统中进程的概念 -- 冯诺依曼体系结构,操作系统,进程概念,查看进程,进程状态,僵尸进程,孤儿进程,进程优先级,进程切换,进程调度

目录 1. 冯诺依曼体系结构 2. 操作系统(Operator System) 2.1 操作系统的概念 2.2 设计操作系统(OS)的目的 2.3 系统调用和库函数概念 3. 进程 3.1 进程的基本概念与基本操作 3.1.1 进程的基本概念 3.1.2 PCB -- 描述进程 3.1.3 task_ struct 3.1.4 查看进程 3.1.5…

4.redis通用命令

文章目录 1.使用官网文档2.redis通用命令2.1set2.2get2.3.redis全局命令2.3.1 keys 2.4 exists2.5 del(delete)2.6 expire - (失效时间)2.7 ttl - 过期时间2.7.1 redis中key的过期策略2.7.2redis定时器的实现原理 2.8 type2.9 object 3.生产环境4.常用的数据结构4.1认识数据类型…

Web项目图片视频加载缓慢/首屏加载白屏

Web项目图片视频加载缓慢/首屏加载白屏 文章目录 Web项目图片视频加载缓慢/首屏加载白屏一、原因二、 解决方案2.1、 图片和视频的优化2.1.1、压缩图片或视频2.1.2、 选择合适的图片或视频格式2.1.3、 使用图片或视频 CDN 加速2.1.4、Nginx中开启gzip 三、压缩工具推荐 一、原因…

成人教育专升本-不能盲目选择

成人教育专升本都有哪些方法?在当今时代,学历往往是打开职业机会的敲门砖,成人教育专升本成为突破职业发展瓶颈的途径,然而,你是否清楚它们之间究竟有着怎样的区别呢? 一、成人教育专升本,成人高考 1、考试形式 成人…

repmgr集群部署-PostgreSQL高可用保证

📢📢📢📣📣📣 作者:IT邦德 中国DBA联盟(ACDU)成员,10余年DBA工作经验, Oracle、PostgreSQL ACE CSDN博客专家及B站知名UP主,全网粉丝10万 擅长主流Oracle、My…

【前端】 canvas画图

一、场景描述 利用js中的canvas画图来画图,爱心、动画。 二、问题拆解 第一个是:canvas画图相关知识。 第二个是:动画相关内容。 三、知识背景 3.1 canvas画图相关内容 canvas画图的基本步骤 获取页面上的canvas标签对象获取绘图上下文…

深度学习——激活函数、损失函数、优化器

深度学习——激活函数、损失函数、优化器 1、激活函数1.1、一些常见的激活函数1.1.1、sigmoid1.1.2、softmax1.1.3、tanh1.1.4、ReLU1.1.5、Leaky ReLU1.1.6、PReLU1.1.7、GeLU1.1.8、ELU 1.2、激活函数的特点1.2.1、非线性1.2.2、几乎处处可微1.2.3、计算简单1.2.4、非饱和性1…

硬件设计-电源轨噪声对时钟抖动的影响

目录 定义 实际案例 总结 定义 首先了解抖动的定义,在ITU-T G.701中有关抖动的定义如下: 数字信号重要瞬间相对于其理想时间位置的短期非累积变化。 抖动是时钟或数据信号时序的短期时域变化。抖动包括信号周期、频率、相位、占空比或其他一些定时特…

搭建自己的wiki知识库(重新整理)

写在前面: 之前写过一篇,因为这次修改的内容比较多,所以不想在原基础上修改了,还是重新整理一篇吧。搭建wiki知识库,可以使用在线文档,如xx笔记、xx文档、xx博客、git仓库(如:GitHu…

数据可视化的Python实现

一、GDELT介绍 GDELT ( www.gdeltproject.org ) 每时每刻监控着每个国家的几乎每个角落的 100 多种语言的新闻媒体 -- 印刷的、广播的和web 形式的,识别人员、位置、组织、数量、主题、数据源、情绪、报价、图片和每秒都在推动全球社会的事件,GDELT 为全…

【Python基础】Python知识库更新中。。。。

1、Python知识库简介 现阶段主要源于个人对 Python 编程世界的强烈兴趣探索,在深入钻研 Python 核心语法、丰富库函数应用以及多样化编程范式的基础上,逐步向外拓展延伸,深度挖掘其在数据分析、人工智能、网络开发等多个前沿领域的应用潜力&…

SpringCloud微服务实战系列:02从nacos-client源码分析注册中心工作原理

目录 角色与功能 工作流程: nacos-client关键源码分析 总结: 角色与功能 服务提供者:在启动时,向注册中心注册自身服务,并向注册中心定期发送心跳汇报存活状态。 服务消费者:在启动时,把注…

电感2222

1 电感 1电感是什么 2 电感的电流不能突变:电容是两端电压不能突变 3 电感只是限制电流变化速度

AI安全漏洞之VLLM反序列化漏洞分析与保姆级复现(附批量利用)

前期准备 环境需要:Linux(这里使用kali)、Anaconda 首先安装Anaconda 前言:最好使用linux,如果使用windows可能会产生各种报错(各种各种各种!!!)&#xff…

用梗营销来启动市场

目录 为什么梗营销适合初创公司 有效的梗营销技巧 梗不仅仅是有趣的图片,它们是包裹在幽默中的文化时刻。对于小企业家(以及大企业家),梗代表了一种强大且性价比高的市场推广方式。让我们分解一下为什么梗营销有效,以…