多线程带来的的风险-线程安全

v2-7bdded212ba4d459fb4fe10bcaa0021d_b

多线程带来的的风险-线程安全

~~ 多线程编程中,最难的地方,也是一个最重要的地方,还是一个最容易出错的地方,更是一个面试中特别爱考的地方.❤️❤️❤️

线程安全的概念

万恶之源,罪魁祸首是多线程的抢占式执行,带来的随机性.~~😕😕😕
如果没有多线程,此时程序代码执行顺序就是固定的,代码顺序固定,程序的结果就是固定的.
如果有了多线程,此时在抢占式执行下,代码执行的顺序,会出现更多的变数!!!
代码执行顺序的可能性就从一种情况变成无数种情况!!!
所以就需要保证这无数种线程调度顺序的情况下,执行的结果都是正确的!!!只要是有一种情况下,代码结果不正确,就视为线程不安全!!!

问题来了:能否消除这样的随机性了🤔🤔🤔?
调度的源头来自于操作系统的内核实现.
1.作为程序猿的我们改不了.😂😂😂
2.即使改了自己的操作系统,也无法推广开来,因为全世界大多数操作系统都是这样的,已成定局!😕😕😕

观察线程不安全(代码)😍😍😍

class Counter{public int count = 0;public void add(){count++;}
}
public class ThreadDemo13 {public static void main(String[] args) {Counter counter = new Counter();// 创建两个线程, 两个线程 counter 来调用 5W 次的add方法Thread t1 = new Thread(()->{for (int i = 0; i < 5_0000; i++) {counter.add();}});Thread t2 = new Thread(()->{for (int i = 0; i < 5_0000; i++) {counter.add();}});// 启动线程t1.start();t2.start();// 等待两个线程结束try {t1.join();t2.join();} catch (InterruptedException e) {throw new RuntimeException(e);}// 打印最终的 count 值 预期结果: count = 10WSystem.out.println("count = "+ counter.count);}
}

运行结果:

image-20230923031309538
我们的需求是两个线程各自自增 5w次,一共自增 10w次,
预期结果是 10w,实际结果不是 10w而且每次都不一样.
程序出现了bug(程序不符合需求,就是bug).
注:这个就是典型的线程安全问题!!!😥😥😥

线程不安全的原因

~~ 为什么程序就出现了这个情况🤔🤔🤔?

线程与指令之间的关系:

一个线程要执行,就需要先编译成很多的CPU指令,写的任何一个代码都是要编译成很多的CPU指令的!!!
个人理解:一个线程是来完成一个任务,要做一些工作,而这个工作是可以分解成一个一个的小步骤的,每个小步骤就是一个指令.
由于线程的抢占式执行,导致当前执行到任意一个指令的时候,线程都有可能被调度走,然后CPU让别的线程来执行.

寄存器,CPU里重要的组成部分,寄存器也能存数据,空间更小,访问速度更快,CPU进行的运算都是针对寄存器(准确的说,是通用寄存器,如EAX,EBX,ECX)中的数据进行的

count++;
++ 操作本质上要分成三步
1.先把内存中的值,读取到CPU的寄存器中 ~~load
2.把CPU寄存器里的数值进行 +1运算 ~~add
3.把得到的结果写回到内存中 ~~ save
注:load,add,save就是CPU上执行的三个指令(被视为机器语言).

两个线程并发的执行count++,此时就相当于两组load,add,save进行执行,此时不同的线程调度顺序就可能会产生一些结果上的差异.

**作图来理解多线程的调度:**❤️❤️❤️

image-20230923125739880

分析执行过程:

image-20230923133152779

思考一下🤔🤔🤔:

出现bug 之后,得到的结果一定是 <= 10w, 结果是一定 >= 5w 嘛?
极端情况下,所有的执行都是交错执行,是否就是 5w 呢??
实际上,结果是可以小于 5w ,只是概率更低了!!
image-20230923133717932

根结底线程安全问题全是因为==线程的无序调度(罪魁祸首,万恶之源)==导致了执行顺序不确定结果就变化了!!!

总结(线程不安全的原因)😊😊😊

  1. [根本原因] 抢占式执行,随机调度 ~~ 对此,我们无能为力.

  2. 代码结构:

    • 多个线程 同时 修改 同一个变量 ~~ 不安全!!! 😥😥😥

    • 一个线程,修改一个变量,安全.

    • 多个线程读取同一个变量,安全.

    • 多个线程修改多个不同的变量,安全.

    • 注1: 因此可以通过代码结构来规避这个线程不安全问题,
      但是因为需求问题,代码结构无法进行调整(这种方法使用频率并不高).

    • 注2: 修改 => 不可变对象是无法修改的,天然就是线程安全的!!!

  3. 原子性. 如果修改操作是原子的,出现问题概率小;如果是非原子的,出现问题概率极高(线程不安全问题,其实本质上是事务的脏读问题,之前博主写的博客解释过相关概念,在此不做解释啦!ヾ(❀^ω^)ノ゙

    • 原子: 不可拆分的基本单位.

      上述 count ++ 操作就不是原子,里面可以拆分成三个操作,load,add,save.某个操作,对应单个CPU指令,就是原子的,如果这个操作对应多个CPU指令,大概率就不是原子的.比如直接使用 = 赋值,就是一个原子的操作

  4. 内存可见性,引起的线程不安全

    • 一个线程读,一个线程改,可能出现读的结果和预期不符合的问题.
  5. 指令重排序,引起的线程不安全

    • 本质上是编译器优化,优化出bug了.优化:编译器觉得程序猿写的代码太low了,对代码进行调整,在保持代码逻辑不变的情况下,调整代码的执行顺序,从而加快程序的执行效率.

线程不安全问题的解决

**如何解决线程不安全问题?**🤨🤨🤨🤨
最主要的手段就是从这个原子性下手,通过加锁不能把非原子的操作变成原子的.
即解决前面代码的线程不安全问题,就是通过加锁让count++变成原子的.
synchronized public void add(){ count++; }

加了 synchronized 之后,进入方法就会加锁,出了方法就会解锁.
如果两个线程同时尝试加锁,此时一个能获取锁成功,另一个只能阻塞等待(处于BLOCKED状态),一直阻塞到刚才的状态解锁(释放锁),当前线程才能加锁成功!!! => 操作系统的基本设定,系统里的锁“不可剥夺”特性,一旦一个线程获取到锁,除非它主动释放,否则无法强占.

加锁,说是保证原子性,不是让load,add,save三个操作一次完成,也不是就是让其它也想操作的线程阻塞等待了
image-20230923205141702

image-20230923203926850

虽然加锁之后,算得慢了,但是还是比单线程要快,加锁只是针对count++加锁了,除了count++之外,还有for循环的代码,for循环代码是可以并发执行的(线程t1和线程t2各自修改各自for循环的局部变量i,是没问题的),只是count++串行执行了.
一个任务中,一部分可以并发,一部分串行,仍然是比所有代码串行要快的.


加锁,是要明确执行对哪个对象加锁的.如果两个线程针对同一个对象加锁,会产生阻塞等待(锁竞争/锁冲突),如果两个线程针对不同对象加锁,不会阻塞等待(不会锁冲突/锁竞争).

synchronized的使用方法😊😊😊😊

  1. 修饰方法
    • 修饰普通方法
      • 进入方法就加锁,离开方法就解锁
    • 修饰静态方法
      • 通理也是这样,进入方法就加锁,离开方法就解锁
    • 但是这两种修饰方法,加锁的“对象”不同,修饰普通方法,锁对象就是this,修饰静态方法,锁对象就是类对象.
  2. 修饰代码块
    • 显示/手动指定锁对象

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode75-06:移动零

移动零 初版(bug) func moveZeroes(nums []int) {if len(nums) 1{return}// 设置两个指针fp1,fp2分别指向第一个元素和第二个元素&#xff0c;// 两种情况// 假如指针fp1指向元素为零// 1) *fp2!0&#xff0c;则交换。// 2) *fp20&#xff0c;则fp2后移,直至*fp2!0,与fp1交换…

idea如何关闭项目文件显示的浏览器图标

这里写自定义目录标题 1.idea经常项目文件右上角弹出图标2.setting中Tools 取消勾选浏览器 1.idea经常项目文件右上角弹出图标 2.setting中Tools 取消勾选浏览器

深度学习自学笔记二:逻辑回归和梯度下降法

目录 一、逻辑回归 二、逻辑回归的代价函数 三、梯度下降法 一、逻辑回归 逻辑回归是一种常用的二分类算法&#xff0c;用于将输入数据映射到一个概率输出&#xff0c;表示为属于某个类别的概率。它基于线性回归模型&#xff0c;并使用了sigmoid函数作为激活函数。 假设我们…

数据结构与算法-时间复杂度与空间复杂度

数据结构与算法 &#x1f388;1.概论&#x1f52d;1.1什么是数据结构&#xff1f;&#x1f52d;1.2什么是算法&#xff1f; &#x1f388;2.算法效率&#x1f52d;2.1如何衡量一个算法的好坏&#xff1f;&#x1f52d;2.2算法的复杂度&#x1f52d;2.3时间复杂度&#x1f4d6;2…

【数据结构】七大排序算法详解

目录 ♫什么是排序 ♪排序的概念 ♪排序的稳定性 ♪排序的分类 ♪常见的排序算法 ♫直接插入排序 ♪基本思想 ♪算法实现 ♪算法稳定性 ♪时间复杂度 ♪空间复杂度 ♫希尔排序 ♪基本思想 ♪算法实现 ♪算法稳定性 ♪时间复杂度 ♪空间复杂度 ♫直接选择排序 ♪基本思想 ♪算法…

MongoDB【部署 02】mongodb使用配置文件启动、添加为系统服务及自启动(一个报错:[13436][NotMasterOrSecondary])

MongoDB使用配置文件启动、添加为系统服务及设置自启动 1.是什么2.下载安装启动配置2.1 下载2.2 安装2.3 配置2.4 使用配置文件启动 3.设置系统服务及自启动3.1 设置为系统服务3.2 自启动 1.是什么 【以下内容来自ChatGPT3.5】 MongoDB是一个流行的开源文档型数据库管理系统&a…

SpringBoot实战(二十四)集成 LoadBalancer

目录 一、简介1.定义2.取代 Ribbon3.主要特点与功能4.LoadBalancer 和 OpenFeign 的关系 二、使用场景一&#xff1a;Eureka LoadBalancer服务A&#xff1a;loadbalancer-consumer 消费者1.Maven依赖2.application.yml配置3.RestTemplateConfig.java4.DemoController.java 服务…

力扣刷题-链表理论基础

什么是链表 什么是链表&#xff0c;链表是一种通过指针串联在一起的线性结构&#xff0c;每一个节点由两部分组成&#xff0c;一个是数据域一个是指针域&#xff08;存放指向下一个节点的指针&#xff09;&#xff0c;最后一个节点的指针域指向null&#xff08;空指针的意思&a…

金融风控建模常用指标介绍(WOE, IV, KS, PSI)

金融风控建模常用指标介绍&#xff08;WOE, IV, KS, PSI&#xff09; 近期在做金融风控相关项目&#xff0c;有必要把特征和模型的衡量指标总结下&#xff0c;以备不时之需。这次主要介绍4个指标&#xff08;WOE, IV, KS, PSI&#xff09;。 WOE&#xff08;Weight of Evidenc…

力扣-228.汇总区间

AC Code 自己做出来的&#xff0c;代码写的很烂&#xff0c;但是也浅浅记录一下叭&#xff0c;下面有看答案思路写出来的双指针代码 class Solution { public:vector<string> summaryRanges(vector<int>& nums) {vector<string> ans;int n nums.size();…

上市公司-供应链数字化示范名单匹配(2000-2022年)

参考《经济管理》刘海建&#xff08;2023&#xff09;、《中国软科学》张树山&#xff08;2021&#xff09;的做法&#xff0c;将商务部公开的“供应链创新与应用试点企业、试点城市”分别与上市公司匹配&#xff0c;得到2份DID数据 一、数据介绍 数据名称&#xff1a;上市公司…

FPGA:卷积编码及维特比译码仿真

FPGA&#xff1a;卷积编码及维特比译码仿真 本篇记录一下在FPGA中完成卷积编码和维特比译码的过程&#xff0c;通过代码解释编码的过程和译码的过程&#xff0c;便于理解&#xff0c;同时也方便移植到其他工程中。 1. 准备工作 卷积编译码IP核—convolutionIP核和viterbiIP核…

工作流 Flowable 的使用

一、BPMN 业务流程建模与标注 通过 Status&#xff08;状态&#xff09; 字段维护流程状态&#xff0c;流程负责的审批人可能也是 Hard Code&#xff08;硬编码&#xff09;会出现以下问题&#xff1a; 1.流程健壮性差&#xff0c;但凡出现人员变动&#xff0c;或者组织结构调…

Linux部署项目

本文以人人权限管理系统为例&#xff0c;使用finalshell工具连接服务器。服务器使用的是腾讯云服务器。用自己虚拟机也可以完成项目部署。 后端代码renren-security: 采用SpringBoot2、MyBatis-Plus、Shiro框架&#xff0c;开发的一套权限系统&#xff0c;极低门槛&#xff0c…

【RocketMQ】(五)消息的消费

消费者从Broker拉取到消息之后&#xff0c;会将消息提交到线程池中进行消费&#xff0c;RocketMQ消息消费是批量进行的&#xff0c;如果一批消息的个数小于预先设置的批量消费大小&#xff0c;直接构建消费请求ConsumeRequest将消费请求提交到线程池处理&#xff0c;否则需要分…

OpenMesh 网格平滑

文章目录 一、简介二、相关参数二、实现代码三、实现效果参考资料一、简介 由于物理采样过程固有的局限性,三维扫描仪获得的网格通常是有噪声的。为了消除这种噪声,所谓的平滑算法被开发出来。这类方法有很多,OpenMesh主要为我们提供了两种平滑算法,一种是较为经典的Laplac…

火山引擎 ByteHouse:ClickHouse 如何保证海量数据一致性

背景 ClickHouse是一个开源的OLAP引擎&#xff0c;不仅被全球开发者广泛使用&#xff0c;在字节各个应用场景中也可以看到它的身影。基于高性能、分布式特点&#xff0c;ClickHouse可以满足大规模数据的分析和查询需求&#xff0c;因此字节研发团队以开源ClickHouse为基础&…

【【萌新的FPGA学习之实战流水灯】】

萌新的FPGA学习之实战流水灯 实验任务 本节的实验任务是使用领航者底板上的两个 PL LED 灯顺序点亮并熄灭&#xff0c;循环往复产生流水灯的效 果&#xff0c;流水间隔时间为 0.5s。 1MHz&#xff1d;1000000Hz 10的6次方 1ns&#xff1d;10的-9次方秒 开发板晶振50Mhz 计算得…

NIO简单介绍

一、什么是NIO 1、Java NIO全称java non-blocking IO&#xff0c; 是指JDK提供的新API。从JDK1.4开始&#xff0c;Java提供了一系列改进的输入/输出的新特性&#xff0c;被统称为NIO(即New IO)&#xff0c;是同步非阻塞的 2、NIO有三大核心部分: Channel(通道)&#xff0c; Buf…

Goland设置头注释

package ${GO_PACKAGE_NAME} * Author: 坐公交也用券 * HomePage: https://liumou.site * File: ${NAME}.go * Date: ${DATE} ${TIME} * Des: 文件作用