线程安全问题

逃离我推掉我的手 2023-09-27 13:11 114阅读 0赞

在这里插入图片描述

文章目录

  • 一、线程安全问题
  • 二、线程安全问题原因
    • 修改共享数据
    • 原子性
    • 内存可见性
    • 指令重排序
  • 三、解决线程安全问题

一、线程安全问题

多线程带给我们效率提升的同时,也为我们带来了风险-线程安全,因为多线程的抢占式执行,带来的随机性。
我们想使用两个线程将一个变量同时增加5000次

  1. class Num{
  2. public int num;
  3. public void add() {
  4. this.num++;
  5. }
  6. }
  7. public class ThreadDemo {
  8. public static void main(String[] args) {
  9. Num num = new Num();
  10. //创建两个线程,分别对调用5000次add()
  11. Thread t1 = new Thread(() -> {
  12. for (int i = 0; i < 5000; i++) {
  13. num.add();
  14. }
  15. });
  16. Thread t2 = new Thread(() -> {
  17. for (int i = 0; i < 5000; i++) {
  18. num.add();
  19. }
  20. });
  21. //启动线程
  22. t1.start();
  23. t2.start();
  24. try {
  25. //等待线程结束
  26. t1.join();
  27. t2.join();
  28. } catch (InterruptedException e) {
  29. throw new RuntimeException(e);
  30. }
  31. System.out.println("num = "+num.num);
  32. }
  33. }

在这里插入图片描述
在这里插入图片描述
为什么程序会出来这个情况?
在这里插入图片描述

++操作本质上要分为三步
1.先把内存中的值读取到CPU的寄存器中 load指令
2.把CPU寄存器里的数值+1操作 add指令
3.把结果值保存到内存中 save指令

此时两个线程的并发执行,就相当于两组load add save指令并发执行。所以这两组指令的执行顺序存在了许多可能性。
这里我举一个线程安全的例子和线程不安全的例子。
在这里插入图片描述
在这里插入图片描述
自增两次,结果为2,是线程安全的。
在这里插入图片描述
因为t1已经load但是还没有add,save,t2也load了,所以t1,t2执行完之后是1(这里和事务的脏读问题是一样的,read uncommitted,t2读到的是t1还没来得及提交的脏数据)
1. 那基于这两个线程有这么多不安全的情况,是否可能结果正好是1w呢?
也是存在可能性的。
在这里插入图片描述
虽然存在这样执行5000次的可能,但概率十分地小。
2.当前这个结果一点大于5000吗?
在这里插入图片描述
当两个线程调度出现上述情况,t2自增两次或多次,t1自增一次,最后还是加1.

二、线程安全问题原因

最根本的原因: 抢占式执行,随机调度

修改共享数据

在这里插入图片描述
我们上述线程代码之所以不安全,因为涉及到我们两个线程同时去修改一个相同的变量。
一个线程修改一个变量,安全
多个线程读取同一个变量,安全
多个线程修改多个不同的变量,安全

原子性

什么是原子性?
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

我们刚才所进行的++操作就不是原子的,由三步组成,也就造成了线程问题,t1结果还没提交,t2就读了。

不保证原子性会给多线程带来什么问题?
一个线程正在对一个变量操作,中途其他线程插进来了,对这个操作造成了打断,可能会造成结果的错误。这和线程的抢占式调度有关,如果不是抢占式,就算不是原子性,也问题不大。

内存可见性

java内存模型(JMM): java虚拟机规定了java内存模型。
目的: 屏蔽各种硬件和操作系统的内存访问差异,实现java程序在各平台下都达成一致的并发效果。
在这里插入图片描述
1.线程之间的共享变量存在 主内存 (Main Memory).
2.每一个线程都有自己的 “工作内存” (Working Memory) .
3.当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
4.当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
一旦线程t1修改了变量,但未来得及同步,对应t2读取工作内部的值就可能出现错误。
在这里插入图片描述
这个时候代码就会出现问题。
为什么要整这么多内存?
其实没有这么多内存,只能java中进行了”抽象”的叫法。
主内存: 硬件角度的内存
工作内存: CPU寄存器 和 高度缓冲器Cache

为什么要复制来复制去?
因为在内存读取速度相比于工作内存的读取速度慢了好几个数量级,但是工作内存有十分小(而且还贵),所以就得不断地复制,将急需的一些东西放在里面。

指令重排序

指令重排序实际上也是编译器优化,简单的来说,就是我们把一个东西写的太烂了,JVM.CPU指令集会对其进行优化。
编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

三、解决线程安全问题

我们采取的策略是针对原子性入手,来解决上述线程安全问题。
通过加锁,把不是原子的转成”原子的”.

  1. class Num{
  2. public int num;
  3. public synchronized void add() {
  4. this.num++;
  5. }
  6. }
  7. public class ThreadDemo {
  8. public static void main(String[] args) {
  9. Num num = new Num();
  10. //创建两个线程,分别对调用5000次add()
  11. Thread t1 = new Thread(() -> {
  12. for (int i = 0; i < 5000; i++) {
  13. num.add();
  14. }
  15. });
  16. Thread t2 = new Thread(() -> {
  17. for (int i = 0; i < 5000; i++) {
  18. num.add();
  19. }
  20. });
  21. //启动线程
  22. t1.start();
  23. t2.start();
  24. try {
  25. //等待线程结束
  26. t1.join();
  27. t2.join();
  28. } catch (InterruptedException e) {
  29. throw new RuntimeException(e);
  30. }
  31. System.out.println("num = "+num.num);
  32. }
  33. }

在这里插入图片描述
在这里插入图片描述
我们可以发现,程序达到了我们预期的效果。
在这里插入图片描述
我们代码的唯一不同,就是此方法加了个synchronized关键字,表示加锁。
加入了synchronized后,进入add()方法就会加锁,出方法后就会解锁。当一个线程获取到锁之后,另一个线程想要获取锁,只能阻塞等待,知道另一个线程释放锁,当前线程才能成功加锁
在这里插入图片描述
lock的阻塞,把t2 的load 推迟到t1的save之后,避免了脏读操作。
此处的原子性,并不是说,让load add save这三个指令操作一步完成,而是在进行这三步操作时,不再进行调度,让其他线程进行阻塞等待。
同时我们进行了加速后,数据的准确性提高了,同时执行效率也降低了,但仍然比单线程快很多。

发表评论

表情:
评论列表 (有 0 条评论,114人围观)

还没有评论,来说两句吧...

相关阅读

    相关 线安全问题

    我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就...

    相关 线安全问题

    一、线程安全 VS 线程不安全? 线程安全指的是代码若是串行执行和并发执行的结果完全一致,就称为该代码是线程安全的。 若多个线程串行执行(单线程执行)的结果和并发执行的

    相关 线安全问题

    定义 > 首先大家需要思考一下何为线程安全性呢??? 《Java并发编程实战》书中给出定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替

    相关 线安全问题

    线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污