当前位置: 首页 > news >正文

java--多线程--线程安全

本篇我们来讲线程安全的知识~~

1.线程安全

所谓线程安全,其实就是一段代码在并发执行的情况之下,出现了bug,这就是线程不安全,反之,如果没有bug就是线程安全~~(bug就是实际结果不满足预期结果)

我们来用一个小案例来理解一下~

package tet; public class gdg2 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1线程结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++){ count++; } System.out.println("t2线程结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }

可以看到,我们在这个程序中使用了两个线程,都对一个count进行累加操作,按道理来说应该是执行两个5万次,结果就应该是十万,但是我们运行之后却是一个随机值,但是就是到不了十万

我们运行三次,每次都不是我们预期的10万,这就是线程不安全的,也就是出了bug~

那我们该怎么解决这个问题呢?来看一个代码案例

package tet; public class gdg2 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1线程结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++){ count++; } System.out.println("t2线程结束"); }); t1.start(); t1.join(); t2.start(); t2.join(); System.out.println(count); } }

可以看到,我们在最后线程的启动这个地方做了手脚,让他串行执行,一个一个来,这个时候我们的运行结果就回归到了正常的10万

造成这个情况的原因呢其实就是因为看似count++是一行代码,但是实际上对应到了三个cpu指令~ 分别是:

  1. load:将内存中的count的值加载到寄存器
  2. add:将寄存器中的内容+1
  3. save:把寄存器中的内容保存回内存上

我们都知道,线程是抢占式执行的,也就是说不一定会完全按照这个顺去来。

就比如说线程1和线程2,count从0开始,线程1先执行load,到了add的时候被线程2抢了,线程2执行完所有操作结果也就是1了,此时又轮到线程1,线程1在刚开始拿到的是0,这个时候还是对0执行+1操作,最后结果还是1,这个时候执行了2次,但是最后的结果却是1~~也就是说,线程抢占式执行是不可预期的,也是构成线程安全问题的罪魁祸首!!

不只是上面说的那一个~有超级多的情况~

这个图只是考虑这两个线程的情况,实际上每个线程调度走,都有可能有其他更多的线程,甚至是别的进程的线程占用cpu执行.........

总结一下这个案例:

  1. 线程的抢占式执行:这是最根本的原因,操作系统可以在任一时间点暂停一个线程,并且切换到另一个线程。这种调度的不可预测性使得代码的执行顺序变的不确定
  2. 多线程对共享变量的修改:如果多个线程只是读取同一个数据,那么就不会出现问题,但是一旦有线程尝试修改这个共享数据,就有可能引发冲突,在这个例子中,count就是t1和t2共享的变量
  3. 非原子性操作:像count++这个看似简单的操作,其实背后却是由多个步骤组成的(读-改-写)组成的。这些步骤合在一起被称为“原子组合”的集合,如果这个操作集合在执行过程中被中断,就有可能导致结果不正确。
  4. 内存可见性问题:为了优化效率,每个CPU通常都有自己的缓存。一个线程对共享变量的修改,可能暂时还保存在自己的缓存中,而没有立即刷新到主内存中。这导致其他线程读取到的仍然是旧的,导致结果不正确。(就比如上面的例子,执行两步还是1)
  5. 指令重排序:为了提高程序性能,编译器和处理器可能会对指令进行重排序。在单线程环境下,这保证了最终结果与代码顺序的执行结果一致。但是在多线程的情况下,重排序可能导致一个线程看到的从操作顺序与另一个线程实际执行的顺序完全不同,从而引发逻辑错误。

上面说了一些还没有讲的东西,这里来补充一下

什么是原子性:

  • 我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进入 房间之后,还没有出来;B是不是也可以进房间,打断A在房间里的隐私。这个就是不具备原子性 的。 那我们应该如何解决这个问题呢?是不是只要给房间加把锁,A进去就把门锁上,其他人是不是就进 不来了。这样就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

什么是可见性:

  • 可见性指的是一个线程对共享变量值的修改,能够及时地被其他线程看到。
  • JVM中规范定义了Java内存模型,目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一直读的并发效果
  • 线程之间的共享变量存在主内存(Main Memory)
  • 每一个线程都有自己的工作内存(Working Memory)
  • 当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存中,再从工作内存读取数据。
  • 当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,在同步回主内存。

由于每个线程都有自己的工作内存,这些工作内存中的内容相当于同一个共享变量的副本,此时修改线程1的工作内存中的值,线程2的工作内存不一定会及时变化。

就比如:初始情况下,两个线程的工作内存内容一致.

一旦线程1修改了a的值,此时主内存不一定能及时同步.对应的线程2的工作内存的a的值也不一定能及时同步.

什么是指令重排序:

⼀段代码是这样的:

1.去前台取下U盘

2.去教室写10分钟作业

3.去前台取下快递

如果实在单线程的情况下,JVM、CPU指令集会对其进行优化,比如:按照1->3->2的方式执行,会少跑一次前台。这种叫做指令重排序。

编译器对于指令重排序的前提是"保持逻辑不发⽣变化".这一点在单线程环境下比较容易判断,但是在多线程环境下就没那么容易了,多线程的代码执行复杂程度更高,编译器很难在编译阶段对代码的执行效果进行预测,因此激进的重排序很容易导致优化后的逻辑和之前不等价.

由于指令重排序涉及到CPU和编译器的一些底层原理,这里不多讲,后续到操作系统时候再讲~

最后再回头看一下最开始的那个线程不安全的代码,最终的代码案例应该是:

package tet; public class gdg2 { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1线程结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++){ count++; } System.out.println("t2线程结束"); }); // 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, // 就开始打印了. 很可能打印出来的 count 就是个 0 t1.start(); t1.join(); t2.start(); t2.join(); System.out.println(count); } }

本篇仅对线程安全进行大概讲解,后续在案例中涉及到了线程安全的时候回拎出来讲~

http://www.jsqmd.com/news/484680/

相关文章:

  • 在openSUSE-Leap-15.6-DVD-x86_64中使用gnome-builder-45.0的基本功能(二)空白Makefile工程
  • AI在线客服系统源码独立管理后台,自动回复文本、图片、视频等多种消息类型
  • 【负载均衡oj项目】03. compile_server编译运行服务设计
  • 大模型学习宝典:零基础入门到项目实战的完整攻略
  • CNN - BiLSTM实现多变量/时间序列预测:Matlab轻松上手
  • 打卡信奥刷题(2956)用C++实现信奥题 P5923 [IOI 2004] empodia 障碍段
  • 基于PID控制的步进电机控制系统仿真:Matlab Simulink的奇妙之旅
  • 打卡信奥刷题(2957)用C++实现信奥题 P5924 [IOI 2004] Phidias 菲迪亚斯神
  • 三维钢板上SH0模态的超声检测建模手记
  • “HALCON error #2404: Invalid handle type in operator do_ocr_multi_class_cnn
  • 基于RSSI加权质心定位算法:将RSSI值转换为距离并优化精确度的新方法
  • 抽象类接口内部类
  • 用大模型和RAG打造智能客服系统,小白也能轻松上手
  • 转载 Java内部类详解
  • 416. 分割等和子集-day39
  • RAG技术解析:让大模型从“闭卷考试“到“开卷考试“的进化
  • 小白的C语言之路(4)——指针运算与动态内存分配
  • Thinkphp和Laravel框架微信小程序的小区废品收购管理系统-
  • Thinkphp和Laravel框架微信小程序的手机银行储蓄业务系统的设计与实现
  • 先甩个最核心的计数器代码镇楼
  • 收藏!小白程序员快速入门:用Agent Skills让大模型能力可复用、可管理
  • 电导增量法INC仿真模型,作为目前实际光伏发电系统中最常用的mppt算法,可以用于学习研究
  • 【跟韩工学Hadoop系列第4篇】004篇-Hadoop 集群搭建-001篇
  • DEF CON CTF Annelid Challenge 深度解析
  • 2026本地口碑佳老火锅品牌排行,看看有你爱吗,重庆火锅/火锅/美食/川渝火锅/火锅店/老火锅,老火锅品牌排行榜单 - 品牌推荐师
  • 零基础搞定 PVE SPICE:远程更流畅 + 文件共享
  • 【C++】C++类的幕后高手:友元、内部类、匿名对象与编译器优化深度解析
  • 常用反弹shell简单分析
  • 玩转T-Mats库:航空发动机气路故障仿真那些事儿
  • DEF CON CTF Sudo Make Me a Sandwich —— 从权限边界到特权执行链的完整攻防复盘