(多线程)线程安全和线程不安全 产生的原因 synchronized关键字 synchronized可重入特性死锁 如何避免死锁 内存可见性

线程安全问题产生原因 

线程安全问题主要发生在多线程环境下,当多个线程同时访问共享资源时,
如果没有采取适当的同步措施,就可能导致数据不一致或程序行为异常

1.[根本]操作系统对于线程的调度是随机的.抢占式执行,这是线程安全问题的罪魁祸首
随机调度使一个程序在多线程环境下,执行顺序存在很多的变数.
程序猿必须保证在任意执行顺序下,代码都能正常工作.
2.多个线程同时修改同一个变量

抢占式执行策略
最初诞生多任务操作系统的时候,非常重大的发明
后世的操作系统,都是一脉相承

t1和t2线程都在修改同一个值:修改的是同一个内存空间

如果是一个线程修改一个变量--没问题
如果是多个线程,不是同时修改同一个变量--没问题
如果多个线程修改不同变量--没问题:不会出现中间结果相互覆盖的情况
如果多个线程读取同一个变量--没问题

变量进行修改.
上面的线程不安全的代码中,涉及到多个线程针对count变量进行修改,此时这个count是一个多个线程都能访问到的"共享数据"

3.修改操作,不是原子的

如果修改操作,只是对应到一个cpu指令,就可以认为是原子的
cpu不会出现"一条指令执行一半"这样的情况的
如果对应到多个cpu指令,就不是原子的

4.内存可见性问题引起的线程不安全
5.指令重排序引起的线程不安全

线程安全问题
一段代码,在多线程中,并发执行后,产生bug.
2.原因
1)操作系统对于线程的调度是随机的.抢占式执行[根本]
2)多个线程同时修改同一个变量
3)修改操作不是原子的
4)内存可见性->编译器优化
5)指令重排序
原子性介绍

什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证A进入房间之后,还没有出来;B是不是也可以进入房间,打断A在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A进去就把门锁上,其他人就进
不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条java 语句不一定是原子的,也不一定只是一条指令
是由三步操作组成的:
1.从内存把数据读到CPU
2.进行数据更新
3.把数据写回到CPU
不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断,这个过程的结果很有可能是错误的。
这点也和线程的抢占式调度密切相关.如果线程不是"抢占"的,就算没有原子性,也问题不大.
 

如何解决线程安全问题

根据它产生的原因来解决:
1.[根本]操作系统对于线程的调度是随机的.抢占式执行
操作系统的底层设定.咱们左右不了

我能否自己写个操作系统,取缔抢占式执行,不就解决线程安全问题了嘛??
理论上当然可行,实际上难度太大了  1)技术上本身就非常难  2)推广上难上加难.
 

2.多个线程同时修改同一个变量,和代码的结构直接相关
调整代码结构,规避一些线程不安全的代码的,但是这样的方案,不够通用.
有些情况下,需求上就是需要多线程修改同一个变量的

超买/超卖的问题--某个商品,库存100件,能否创建出101个订单?

Java中有个东西
String就是采取了"不可变'特性,确保线程安全.
String是咋样实现的"不可变"效果??-private修饰

而是说,String没有提供public 的修改方法. 和final没有任何关系!!
String的final用来实现"不可继承'

3.修改操作,不是原子的.
Java中解决线程安全问题,最主要的方案.-----加锁
通过加锁操作,让不是原子的操作,打包成一个原子的操作.

计算机中的锁,和生活中的锁,是同样的概念.互斥/排他

    其他人就得等待

把锁"锁上"称为"加锁"     把锁"解开"称为"解锁"
一旦把锁加上了,其他人要想加锁,就得阻塞等待

就可以使用锁,把刚才不是原子的count++包裹起来.
在count++之前,先加锁.然后进行count++.计算完毕之后,再解锁

执行3步走过程中,其他线程就没法插队了~~
加锁操作,不是把线程锁死到cpu上,禁止这个线程被调度走
但是是禁止其他线程重新加这个锁,避免其他线程的操作在当前线程执行过程中,插队

加锁/解锁本身是操作系统提供的api,很多编程语言都对于这样的api进行封装了.
大多数的封装风格,都是采取两个函数
加锁lock();//执行一些要保护起来的逻辑
解锁 unlock();

synchronized关键字

Java 中,使用synchronized这样的关键字,搭配代码块,来实现类似的效果的.
//进入代码块,就相当于加锁
synchronized{
//执行一些要保护的逻辑
}//出了代码块,就相当于解锁

()填写啥呢??    填写的是,用来加锁的对象.
要加锁,要解锁,前提是得先有一个锁  在Java中,任何一个对象,都可以用作"锁:

这个对象的类型是啥,不重要
重要的是,是否有多个线程尝试针对这同一个对象加锁(是否在竞争同一个锁)

两个线程,针对同一个对象加锁,才会产生互斥效果.
(一个线程加上锁了,另一个线程就得阻塞等待,等到第一个线程释放锁,才有机会)

下面这种不是同一个锁:

如果是不同的锁对象,此时不会有互斥效果,线程安全问题,没有得到改变的.

解决线程安全问题,不是你写了synchronized就可以.
而是要正确的使用锁~~
1)synchronized{}代码块要合适.
2)synchronized()指定的锁对象也得合适.

这俩线程并发执行过程中,相当于只有count++这个操作,会涉及到互斥
for循环里的条件判断(i<50000)和i++这俩操作不涉及到互斥

意味着整个for循环,ir<50000,i++,count++   都是"互斥"的方式执行的

如果t2是后获取锁  t1就已经lock完成了.  t2的lock就会阻塞.
等到t1执行完unlock  t2才会继续执行

保证每次循环内部的count++  在两个线程之间是串行执行的

这个写法中,只是每次count++之间是串行的for中的i<5w和i++则是并发的.执行速度更快

Java 中为啥使用synchronized+代码块做法?
而不是采用lock+unlock函数的方式来搭配呢?

就是为了防止unlock这个没有写,代码中间抛出异常  也可能使unlock执行不到

Java采取的synchronized,就能确保,只要出了}一定能释放锁.无论因为return还是因为异常
无论里面调用了哪些其他代码,都是可以确保unlock操作执行到的.

使用synchronized修饰add方法 相当于对该方法进行加锁

多个线程针对同一个对象加锁   才会产生互斥(锁冲突/锁竞争)

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

synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待.

进入synchronized修饰的代码块,相当于加锁
退出synchronized修饰的代码块,相当于解锁

死锁
一旦代码触发了死锁,此时线程就卡住了.
原因
1)互斥
2)不可剥夺/不可抢占
3)请求和保持
4)循环等待
解决死锁
1)避免锁嵌套=>打破3)
2)约定加锁顺序=>打破4)
synchronized可重入特性 

 synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

理解"把自己锁死"----一个线程没有释放锁,然后又尝试再次加锁.
//第一次加锁,加锁成功
lock();
//第二次加锁,锁已经被占用,阻塞等待.
lock();

阻塞等待   等到前一次加锁被释放,第二次加锁的阻塞才会接触(继续执行)

看起来是两次一样的加锁,没有必要.   但是实际上开发中,很容易写出这样的代码的

一旦方法调用的层次比较深,就搞不好容易出现这样的情况

要想解除阻塞,需要往下执行才可以.   要想往下执行,就需要等到第一次的锁被释放

这样的问题,就称为"死锁"

1.第一次进行加锁操作,能够成功的(锁没有人使用)
2.第二次进行加锁,此时意味着,锁对象是已经被占用的状态.第二次加锁,就会触发阻塞等待

为了解决上述的问题,Java的synchronized就引入了可重入的概念.

当某个线程针对一个锁,加锁成功之后   后续该线程再次针对这个锁进行加锁,
不会触发阻塞,而是直接往下走     因为当前这把锁就是被这个线程持有
但是,如果是其他线程尝试加锁,就会正常阻塞

死锁是一个非常严重的bug.使代码执行到这一块之后,就卡住.

可重入锁的实现原理,关键在于让锁对象内部保存,当前是哪个线程持有的这把锁
后续有线程针对这个锁加锁的时候,对比一下,锁持有者的线程是否和当前加锁的线程是同一个

如何自己实现一个可重入锁?
1.在锁内部记录当前是哪个线程持有的锁.后续每次加锁,都进行判定
2.通过计数器,记录当前加锁的次数,从而确定何时真正进行解锁.

先引入一个变量,计数器(0)
每次触发{的时候把计数器 :++
每次触发}的时候,把计数器--
当计数器--为0的时候,就是真正需要解锁的时候

死锁

关于死锁
一个线程,一把锁,连续加锁两次
两个线程,两把锁,每个线程获取到一把锁之后,尝试获取对方的锁

通俗来讲就是:两者互不相让 就会构成死锁

 必须是,拿到第一把锁,再拿第二把锁.(不能释放第一把锁)

此时我们查看控制台

该线程,因为竞争锁的缘故而阻塞了.

这样就构成了死锁

如果不加sleep,很可能t1一口气就把locker1和locker2都拿到了.这个时候,t2还没开动呢~~
自然无法构成死锁.

死锁的概率 和当前电脑的运行环境有关系的
看你的当前机器上运行的任务多不多,系统调度的频次是怎样的.......

死锁的第三种情况.N个线程M把锁.
一个经典的模型,哲学家就餐问题

如何避免死锁

如何避免代码中出现死锁呢?
死锁是怎样构成的   构成死锁的四个必要条件(重要)
1.锁是互斥的.一个线程拿到锁之后,另一个线程再尝试获取锁,必须要阻塞等待.
 

2.锁是不可抢占的.线程1拿到锁,线程2也尝试获取这个锁,线程2必须阻塞等待
(不可剥夺)而不是线程2直接把锁抢过来

互斥是指同一时间只能有一个线程持有锁 不可抢占是指线程获取锁之后,其它线程不能强制剥夺锁 只能等它主动释放

至少,Java 的synchronized是遵守这两点

除非是你自己实现一个锁,解决特定的问题
可以打破这两点.至少各种语言内置的锁/主流的锁实现,都是会遵守这两点

3.请求和保持.一个线程拿到锁1之后,不释放锁1的前提下,获取锁2
如果先放下左手的筷子,再拿右手的筷子,就不会构成死锁
4.循环等待.多个线程,多把锁之间的等待过程,构成了"循环"
A等待B,B也等待A或者A等待B,B等待C,C等待A

破坏掉上述的3或者4任何一个条件   都能够打破死锁

有些情况下,确实是需要拿到多个锁,再进行某个操作的.(嵌套,很难避免)

所以 第三步有时候是不能打破的

约定,每个线程加锁的时候   永远是先获取序号小的锁   后获取序号大的锁

约定好加锁的顺序,就可以破除循环等待了.

死锁的小结
1.构成死锁的场景
a)一个线程一把锁=>可重入锁
b)两个线程两把锁=>代码如何编写
c)N个线程M把锁=>哲学家就餐问题
2.死锁的四个必要条件
a)互斥b)不可剥夺c)请求和保持d)循环等待
3.如何避免死锁
打破c)和d)

也不是写了synchronized就100%线程安全.  得具体代码具体分析

这三个兄弟,虽然有synchronized.     不推荐使用.
加锁这个事情,不是没有代价的.
一旦代码中,使用了锁,意味着代码可能会因为锁的竞争,产生阻塞=>程序的执行效率大打折扣.

线程阻塞=>从cpu上调度走.    啥时候能调度回来继续执行????不好说了

内存可见性 

可见性指,一个线程对共享变量值的修改,能够及时地被其他线程看到.

内存可见性是造成线程安全问题的原因之一. 

虽然输入了非0的值    但是此时t1线程循环并没有结束.
t1线程持续执行

很明显,这个也是bug--------线程安全问题.
一个线程读取,一个线程修改--------修改线程修改的值,并没有被读线程读到.
"内存可见性问题"

编译器,虽然声称优化操作,是能够保证逻辑不变.尤其是在多线程的程序中,编译器的
可能导致编译器的优化,使优化后的逻辑,和优化前的逻辑出现细节上的偏差:
研究JDK的程序员,就希望通过让编译器&JVM对程序员写的代码,自动的进行优化
本来写的代码是进行xxxxx,编译器/VM会在你原有逻辑不变的前提下,对你的代码进行调整.
使程序效率更高

编译器,虽然声称优化操作,是能够保证逻辑不变.尤其是在多线程的程序中,编译器的判断可
可能导致编译器的优化,使优化后的逻辑,和优化前的逻辑出现细节上的偏差

上面的这个循环操作:

短时间之内,这个循环,就会循环很多次

load是读内存操作  cmp是纯cpu 寄存器操作
load的时间开销可能是cmp的几千倍

jvm:执行这么多次读flag的操作   发现值始终都是0.
既然都是一样的结果既然还要反复执行这么多次
于是就把读取内存的操作,优化成读取寄存器这样的操作
(把内存的值读到寄存器了.后续再load不再重新读内存,直接从寄存器里来取)

于是,等到很多秒之后,用户真正输入新的值,真正修改flag,
此时t1线程,就感知不到了.(编译器优化,使得t1线程的读操作,不是真正读内存

修改一下上面的代码:

本来这个循环,转的飞起   1s钟几千万次,上亿次.....
但是加了sleep(1)之后    循环次数大幅度降低了.
当引入 sleep 之后,sleep消耗的时间相比于上面load flag的操作,就高了不知道多少了.
假设本身读取flag的时间是1ns的话,如果把读内存操作优化成读寄存器,1ns=>0.xxns,优化50%以上
如果引入sleep,sleep直接占用1ms.此时又不优化flag无足轻重.

所以就不会进行优化操作了

编译器的优化,本身是一个比较复杂的工程
具体怎么优化,咱们作为普通程序员很难感知到

针对内存可见性问题,也不能指望通过sleep来解决
使用sleep大大影响到程序的效率.
希望,不使用sleep也能解决上述的内存可见性问题呢?

在语法中,引入volatile关键字:通过这个关键字来修饰某个变量,此时编译器这对这个变量的读取操作,就不会被优化成都寄存器.

这样的变量的读取操作,就不会被编译器进行优化了.

t2修改了,t1就能及时看到了

volatile解决内存可见性问题   不是解决原子性问题

2)volatile
编译器优化,出bug.
使用这个关键字修饰的变量,就属于"易失""易变"
必须每次重新读取内存中的数据了.

 

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

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

相关文章

defineCustomElement 的局限性及重载需求分析

一、defineCustomElement 的核心局限性 Vue 的 defineCustomElement 虽然实现了 Vue 组件到 Web Components 的转换,但在跨框架/跨语言场景下存在以下关键局限,这也是你的项目需要重载其返回构造器的根本原因: 1. 框架间事件模型不兼容 Vue 事件机制:依赖 $emit 转换的 C…

如何在前端开发中应用AI技术?

一、AI 辅助前端开发流程&#xff08;提效工具&#xff09;智能代码生成与补全使用 AI 编程工具&#xff08;如 GitHub Copilot、Cursor、Amazon CodeWhisperer&#xff09;实时生成代码片段&#xff0c;支持 HTML、CSS、JavaScript、React/Vue 等框架语法。例如&#xff0c;输…

极海发布APM32F425/427系列高性能MCU:助力工业应用升级

聚焦工业4.0及能源管理应用对主控MCU的高性能需求&#xff0c;极海正式发布APM32F425/427系列高性能拓展型MCU&#xff0c;集合运算性能、ADC性能、Flash控制器性能与通信接口四大维度革新&#xff0c;进一步增强了EMC性能&#xff0c;重新定义Cortex-M4F内核在复杂工业场景下的…

JSX深度解析:不是HTML,胜似HTML的语法糖

JSX深度解析&#xff1a;不是HTML&#xff0c;胜似HTML的语法糖 作者&#xff1a;码力无边大家好&#xff01;我是依然在代码世界里乘风破浪的码力无边。欢迎回到我们的《React奇妙之旅》第二站&#xff01; 在上一篇文章中&#xff0c;我们成功地用Vite启动了第一个React应用&…

大模型应用新趋势:从思维链到 HTML 渲染的破局之路

一、大模型交互范式的演进&#xff1a;从 Prompt 工程到思维链革新早期的 Prompt 工程曾面临 “模型特异性” 困境 —— 精心设计的提示词在不同模型上效果迥异。但随着 ** 思维链&#xff08;CoT&#xff09;** 技术的成熟&#xff0c;这一局面正在改变。从 OpenAI o1 的隐式整…

从“找不到”到“秒上手”:金仓文档系统重构记

你是否曾在浩如烟海的产品手册中迷失方向&#xff1f;是否为了一个关键参数翻遍十几页冗余说明&#xff1f;是否对时灵时不灵的搜索功能感到抓狂&#xff1f;甚至因为漫长的加载时间而失去耐心&#xff1f;我们懂你!这些曾困扰金仓用户的文档痛点&#xff0c;从现在起&#xff…

【开源项目分享】可监控电脑CPU、显卡、内存等硬件的温度、功率和使用情况

系列文章目录 【开源项目分享】可监控电脑CPU、显卡、内存等硬件的温度、功率和使用情况 &#xff08;一&#xff09;开源的硬件监控工具 LibreHardwareMonitor &#xff08;二&#xff09;LibreHardwareMonitor 分层架构设计 &#xff08;三&#xff09;LibreHardwareMonitor…

帕累托优化:多目标决策的智慧与艺术

本文由「大千AI助手」原创发布&#xff0c;专注用真话讲AI&#xff0c;回归技术本质。拒绝神话或妖魔化。搜索「大千AI助手」关注我&#xff0c;一起撕掉过度包装&#xff0c;学习真实的AI技术&#xff01; 在相互冲突的目标中寻找最优平衡 ✨ 1. 帕累托优化概述 帕累托优化&a…

#Linux内存管理学以致用# 请你根据linux 内核struct page 结构体的双字对齐的设计思想,设计一个类似的结构体

Linux struct page 的双字对齐设计思想1.双字对齐&#xff08;8字节对齐&#xff09;&#xff1a;确保struct page的大小是sizeof(long)的整数倍&#xff08;通常8字节&#xff09;&#xff0c;便于CPU高效访问。减少内存碎片&#xff0c;提高缓存行&#xff08;Cache Line&…

白酒变局,透视酒企穿越周期之道

今年以来&#xff0c;在科技股的带动下&#xff0c;A股市场表现十分突出&#xff0c;近期沪指甚至创出了十年来新高。然而&#xff0c;在这轮市场的表现中&#xff0c;曾经被资金热捧的白酒板块&#xff0c;却显得有些沉寂。业绩层面&#xff0c;从目前已披露的白酒上市公司半年…

智慧园区:从技术赋能到价值重构,解锁园区运营新范式

在数字化浪潮席卷产业的当下&#xff0c;智慧园区已从 “概念蓝图” 落地为 “实战方案”&#xff0c;其核心逻辑既源于技术的突破性应用&#xff0c;也扎根于企业的实际需求&#xff0c;更顺应着行业发展的未来趋势&#xff0c;成为驱动园区从传统管理向智能化运营升级的核心引…

模运算(密码学/算法)

1 什么是模运算 模运算的概念 模运算是一种算术运算&#xff0c;常写作a mod n&#xff0c;表示整数a除以正整数n后的余数。 模数是模运算中的除数n&#xff0c;它决定了结果的范围。 公式表达&#xff1a; 对于任意整数a和正整数n&#xff0c;可以将a表示为&#xff1a;a qn …

海康相机的 HB 模式功能详解

海康相机的 HB 模式是一种无损压缩技术,全称为High Bandwidth 模式,主要用于提升工业相机在高速场景下的数据传输效率。其核心原理是通过硬件级无损压缩算法对原始图像数据进行压缩,在不损失画质的前提下减少数据量,从而突破千兆网络的带宽限制,实现更高的行频和传输帧率。…

electron应用开发:命令npm install electron的执行逻辑

我们来彻底解析 npm install electron 这个命令背后的完整执行逻辑。这是一个非常精妙的过程&#xff0c;远不止下载一个简单的 JavaScript 包那么简单。理解了它&#xff0c;你就能透彻地明白 Electron 开发环境的运作原理&#xff0c;并能轻松解决各种安装问题。 npm instal…

Visual Studio 2022不同项目设置不同背景图

ClaudiaIDE Visual Studio 地址&#xff1a;https://marketplace.visualstudio.com/items?itemNamekbuchi.ClaudiaIDE&ssrfalse#overviewgithub 地址&#xff1a;https://github.com/buchizo/ClaudiaIDE/ 这是一个Visual Studio扩展&#xff0c;可以让你设置自定义背景图…

React页面使用ant design Spin加载遮罩指示符自定义成进度条的形式

React页面使用ant design Spin加载遮罩指示符自定义成进度条的形式具体实现&#xff1a;import React, { useState, useEffect, } from react; import { Spin, Progress, } from antd; import styles from ./style.less;const App () > {// 全局加载状态const [globalLoadi…

TCP并发服务器构建

TCP并发服务器构建&#xff1a; 单循环服务器&#xff1a;服务端同一时刻只能处理单个客户端的任务 并发服务器&#xff1a;服务端同一时刻能够处理多个客户端的任务 产生多个套接字可建立多个连接&#xff1a;TCP服务端并发模型&#xff1a; 1&#xff1a;使用多进程 头文件&a…

优选算法-常见位运算总结

1.基础位运算&#xff1a; >> :右移运算符&#xff1a; 逻辑右移&#xff08;无符号数&#xff09;&#xff1a;高位补 0&#xff0c;低位直接丢弃。 示例&#xff1a;8 >> 2&#xff08;二进制 1000 右移 2 位&#xff09;结果为 0010&#xff08;十进制 2&#…

记一次MySQL数据库的操作练习

数据库基础使用数据库的操作&#xff1a;1.使用命令行连接数据库。在命令行键入”mysql -u root -p”命令。2.列出MySQL数据库管理系统的数据库列表。在命令行键入”show databases;”命令。3.创建数据库。在命令行键入”create database database_name;”命令。使用”show dat…

C++STL-list 底层实现

目录 一、实现框架 二、list_node节点类的模拟实现 节点构造函数 三、list_iterator迭代器的模拟实现 迭代器类的模板参数说明 构造函数 *运算符重载 运算符的重载 --运算符的重载 运算符的重载 !运算符的重载 list的模拟实现 默认成员函数 构造函数 拷贝构造函…