线程安全问题 逃离我推掉我的手 2023-09-27 13:11 13阅读 0赞 ![在这里插入图片描述][f9d8ad49a2a449b9888b716a9946db5c.gif_pic_center] #### 文章目录 #### * 一、线程安全问题 * 二、线程安全问题原因 * * 修改共享数据 * 原子性 * 内存可见性 * 指令重排序 * 三、解决线程安全问题 ## 一、线程安全问题 ## 多线程带给我们效率提升的同时,也为我们带来了风险-线程安全,因为多线程的抢占式执行,带来的随机性。 我们想使用两个线程将一个变量同时增加5000次 class Num{ public int num; public void add() { this.num++; } } public class ThreadDemo { public static void main(String[] args) { Num num = new Num(); //创建两个线程,分别对调用5000次add() Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { num.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { num.add(); } }); //启动线程 t1.start(); t2.start(); try { //等待线程结束 t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("num = "+num.num); } } ![在这里插入图片描述][5f3e807b95594a80a89403690c03531e.png] ![在这里插入图片描述][c2538bd5cda747f1b491eed5d384a6ce.png] 为什么程序会出来这个情况? ![在这里插入图片描述][056fc550bc7e4fddbdfe6562cb3b3bfb.png] > \++操作本质上要分为三步 > 1.先把内存中的值读取到CPU的寄存器中 load指令 > 2.把CPU寄存器里的数值+1操作 add指令 > 3.把结果值保存到内存中 save指令 此时两个线程的并发执行,就相当于两组load add save指令并发执行。所以这两组指令的执行顺序存在了许多可能性。 这里我举一个线程安全的例子和线程不安全的例子。 ![在这里插入图片描述][68ddc791f0e6488ebabe11d1e1df2507.png] ![在这里插入图片描述][057e9f2ff56d4402af4d32bb608b28f0.png] 自增两次,结果为2,是线程安全的。 ![在这里插入图片描述][9bff578ab1e4404fa079a9918efffd30.png] 因为t1已经load但是还没有add,save,t2也load了,所以t1,t2执行完之后是1(这里和事务的脏读问题是一样的,read uncommitted,t2读到的是t1还没来得及提交的脏数据) **1. 那基于这两个线程有这么多不安全的情况,是否可能结果正好是1w呢?** 也是存在可能性的。 ![在这里插入图片描述][249d5d8b9f304e92831565faedcd1bd5.png] 虽然存在这样执行5000次的可能,但概率十分地小。 **2.当前这个结果一点大于5000吗?** ![在这里插入图片描述][9e3e7cf412bf4c77b086b30b810752a5.png] 当两个线程调度出现上述情况,t2自增两次或多次,t1自增一次,最后还是加1. ## 二、线程安全问题原因 ## 最根本的原因: 抢占式执行,随机调度 ### 修改共享数据 ### ![在这里插入图片描述][a03941a298594213bb67f1940ba6c435.png] 我们上述线程代码之所以不安全,因为涉及到我们两个线程同时去修改一个相同的变量。 一个线程修改一个变量,安全 多个线程读取同一个变量,安全 多个线程修改多个不同的变量,安全 ### 原子性 ### **什么是原子性?** 我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。 我们刚才所进行的++操作就不是原子的,由三步组成,也就造成了线程问题,t1结果还没提交,t2就读了。 **不保证原子性会给多线程带来什么问题?** 一个线程正在对一个变量操作,中途其他线程插进来了,对这个操作造成了打断,可能会造成结果的错误。这和线程的抢占式调度有关,如果不是抢占式,就算不是原子性,也问题不大。 ### 内存可见性 ### java内存模型(JMM): java虚拟机规定了java内存模型。 目的: 屏蔽各种硬件和操作系统的内存访问差异,实现java程序在各平台下都达成一致的并发效果。 ![在这里插入图片描述][86b4f59e052148e78aba652a5c34ccd8.png] 1.线程之间的共享变量存在 主内存 (Main Memory). 2.每一个线程都有自己的 “工作内存” (Working Memory) . 3.当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据. 4.当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存. 一旦线程t1修改了变量,但未来得及同步,对应t2读取工作内部的值就可能出现错误。 ![在这里插入图片描述][e81b50a3c4ce4b7d8893b7f0e3312db8.png] 这个时候代码就会出现问题。 **为什么要整这么多内存?** 其实没有这么多内存,只能java中进行了"抽象"的叫法。 主内存: 硬件角度的内存 工作内存: CPU寄存器 和 高度缓冲器Cache **为什么要复制来复制去?** 因为在内存读取速度相比于工作内存的读取速度慢了好几个数量级,但是工作内存有十分小(而且还贵),所以就得不断地复制,将急需的一些东西放在里面。 ### 指令重排序 ### 指令重排序实际上也是编译器优化,简单的来说,就是我们把一个东西写的太烂了,JVM.CPU指令集会对其进行优化。 编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价. ## 三、解决线程安全问题 ## 我们采取的策略是针对原子性入手,来解决上述线程安全问题。 通过加锁,把不是原子的转成"原子的". class Num{ public int num; public synchronized void add() { this.num++; } } public class ThreadDemo { public static void main(String[] args) { Num num = new Num(); //创建两个线程,分别对调用5000次add() Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { num.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { num.add(); } }); //启动线程 t1.start(); t2.start(); try { //等待线程结束 t1.join(); t2.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("num = "+num.num); } } ![在这里插入图片描述][c8bc3196a3824fcb8b804776df618dfd.png] ![在这里插入图片描述][135200f06d434b7094ef0055694dc514.png] 我们可以发现,程序达到了我们预期的效果。 ![在这里插入图片描述][210f3e3f58ce47c6956b1dad2314449d.png] 我们代码的唯一不同,就是此方法加了个synchronized关键字,表示加锁。 加入了synchronized后,进入add()方法就会加锁,出方法后就会解锁。当一个线程获取到锁之后,另一个线程想要获取锁,只能阻塞等待,知道另一个线程释放锁,当前线程才能成功加锁 ![在这里插入图片描述][d8c3670367134918bb70541729e82350.png] lock的阻塞,把t2 的load 推迟到t1的save之后,避免了脏读操作。 此处的原子性,并不是说,让load add save这三个指令操作一步完成,而是在进行这三步操作时,不再进行调度,让其他线程进行阻塞等待。 同时我们进行了加速后,数据的准确性提高了,同时执行效率也降低了,但仍然比单线程快很多。 [f9d8ad49a2a449b9888b716a9946db5c.gif_pic_center]: https://img-blog.csdnimg.cn/f9d8ad49a2a449b9888b716a9946db5c.gif#pic_center [5f3e807b95594a80a89403690c03531e.png]: https://img-blog.csdnimg.cn/5f3e807b95594a80a89403690c03531e.png [c2538bd5cda747f1b491eed5d384a6ce.png]: https://img-blog.csdnimg.cn/c2538bd5cda747f1b491eed5d384a6ce.png [056fc550bc7e4fddbdfe6562cb3b3bfb.png]: https://img-blog.csdnimg.cn/056fc550bc7e4fddbdfe6562cb3b3bfb.png [68ddc791f0e6488ebabe11d1e1df2507.png]: https://img-blog.csdnimg.cn/68ddc791f0e6488ebabe11d1e1df2507.png [057e9f2ff56d4402af4d32bb608b28f0.png]: https://img-blog.csdnimg.cn/057e9f2ff56d4402af4d32bb608b28f0.png [9bff578ab1e4404fa079a9918efffd30.png]: https://img-blog.csdnimg.cn/9bff578ab1e4404fa079a9918efffd30.png [249d5d8b9f304e92831565faedcd1bd5.png]: https://img-blog.csdnimg.cn/249d5d8b9f304e92831565faedcd1bd5.png [9e3e7cf412bf4c77b086b30b810752a5.png]: https://img-blog.csdnimg.cn/9e3e7cf412bf4c77b086b30b810752a5.png [a03941a298594213bb67f1940ba6c435.png]: https://img-blog.csdnimg.cn/a03941a298594213bb67f1940ba6c435.png [86b4f59e052148e78aba652a5c34ccd8.png]: https://img-blog.csdnimg.cn/86b4f59e052148e78aba652a5c34ccd8.png [e81b50a3c4ce4b7d8893b7f0e3312db8.png]: https://img-blog.csdnimg.cn/e81b50a3c4ce4b7d8893b7f0e3312db8.png [c8bc3196a3824fcb8b804776df618dfd.png]: https://img-blog.csdnimg.cn/c8bc3196a3824fcb8b804776df618dfd.png [135200f06d434b7094ef0055694dc514.png]: https://img-blog.csdnimg.cn/135200f06d434b7094ef0055694dc514.png [210f3e3f58ce47c6956b1dad2314449d.png]: https://img-blog.csdnimg.cn/210f3e3f58ce47c6956b1dad2314449d.png [d8c3670367134918bb70541729e82350.png]: https://img-blog.csdnimg.cn/d8c3670367134918bb70541729e82350.png
相关 线程安全问题 我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就... 绝地灬酷狼/ 2024年04月23日 20:34/ 0 赞/ 90 阅读
相关 线程安全问题 作者简介: zoro-1,目前大二,正在学习Java,数据结构,mysql,javaee等作者主页:??。 朴灿烈づ我的快乐病毒、/ 2024年04月20日 04:48/ 0 赞/ 102 阅读
相关 线程安全问题 目录 1.不安全原因 1.1线程调度无序(抢占式) 1.2多个线程修改同一个变量 1.3修改操作不是原子的 1.4内存可见性 1.5指令重排序 2.解决问题 2 缺乏、安全感/ 2024年03月25日 18:16/ 0 赞/ 68 阅读
相关 线程安全问题 一、线程安全 VS 线程不安全? 线程安全指的是代码若是串行执行和并发执行的结果完全一致,就称为该代码是线程安全的。 若多个线程串行执行(单线程执行)的结果和并发执行的 短命女/ 2023年10月15日 17:32/ 0 赞/ 75 阅读
相关 线程安全问题 定义 > 首先大家需要思考一下何为线程安全性呢??? 《Java并发编程实战》书中给出定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替 旧城等待,/ 2023年10月01日 19:02/ 0 赞/ 25 阅读
相关 线程安全问题 ![在这里插入图片描述][f9d8ad49a2a449b9888b716a9946db5c.gif_pic_center] 文章目录 一、线程安全问题 二、 逃离我推掉我的手/ 2023年09月27日 13:11/ 0 赞/ 14 阅读
相关 线程安全问题 基本概述: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nL àì夳堔傛蜴生んèń/ 2022年11月29日 12:21/ 0 赞/ 247 阅读
相关 线程安全问题 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污 迷南。/ 2022年08月21日 04:15/ 0 赞/ 269 阅读
相关 线程安全问题 线程安装概念 当多个线程,访问某一个类,对象或者方法时 这个类始终都能表现出正确的行为 那么,这个类,对象或者方法,就是线程安全的 Synchronize 朴灿烈づ我的快乐病毒、/ 2022年05月17日 08:06/ 0 赞/ 291 阅读
相关 线程安全问题 a++造成运行结果错误 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly ╰半橙微兮°/ 2021年09月24日 02:46/ 0 赞/ 460 阅读
还没有评论,来说两句吧...