当前位置:   article > 正文

线程不安全的原因以及解决方法

线程不安全

线程不安全的原因

首先要了解使用多线程的原因,以 杀毒软件 为例。

  • 使用单线程
    想要执行 病毒查杀清理垃圾,那么只能先执行 病毒查杀清理垃圾 的其中一个,再执行另外一个。
  • 使用多线程
    可以 同时执行 清理垃圾病毒查杀
    但是问题随之而来,下边将解析多线程的安全问题。

抢占式执行(线程安全根本原因)

首先明确线程是 抢占式执行 的,也就是说,CPU 调度线程的时间是不确定的。
还是以 病毒查杀 为例,可能 病毒查杀 这个线程执行到一半,就被调度出 CPU,然后执行 清理垃圾

多个线程修改同一个变量

CPU 修改数据流程(记住指令,下面会提到

  1. load:将内存取数据 count 到寄存器上;
  2. add:执行指令使得 count++;
  3. save:将 count 写入内存。
  • 正常一个 线程A 修改一个数据的流程(以修改 count 为例,count == 0)
    在这里插入图片描述

简化图
在这里插入图片描述

此时 count = 1

  • 如果是两个线程,线程 A线程 B 前面说过, CPU 调度线程的时间是不确定的,假如 线程A 执行到一半,CPU 将他调度出 CPU,然后调度线程 B 进入 CPU 执行。

在这里插入图片描述

简化图
在这里插入图片描述

预期 count = 2
实际 count = 1
这就导致了多线程安全问题,多个线程修改同一个变量 count,由于 CPU 调度的时间不确定,导致实际结果与预期结果不符合的 bug。

还有很多种情况也会导致结果不对 count。
在这里插入图片描述

线程修改不是原子性的

根据 多个线程修改同一个遍历 的例子看来,如果想要线程是安全的,也就是实际与预期一样,那么就需要保证 线程 A 的执行指令(load,add,save)全部执行完成后,再执行线程 B 的指令(load,add,save),这样的话就可以保证结果是对的,也就是线程是安全的。也就是 原子性!(这也是保证线程安全的主要手段)
在这里插入图片描述

内存可见性

首先明确一点,多线程共享资源,放在这个场景中,那就是共享一片内存。
多个线程修改同一个变量 例子一样,线程 A 依然是执行 修改 操作,但是 **线程 B ** 执行的是 读取操作,如果线程 A 还在 CPU 执行,也就是说 count 数据还没写入内存,此时线程 B 去读取内存的数,依然还是 0。同样也会导致线程安全问题。

对于线程 A 还在 CPU 执行,如果 count++ 一次,可能不会出现这样的情况,但是如果是 count++ 十万次,由于 CPU 从内存中读取数据的时间相对于从寄存器中读取数据的时间是慢了很多很多的,而编译器会在整体逻辑条件不变的情况下对生成的指令做出一些调整,整体的指令可能会变成这样 load add add add … save,如果线程 B 去读取内存中的 count 时,可能线程 A 还在 CPU 执行 add,还没有执行 save(写入内存),因此读到的数据还是 0;和预期的不一样。
解决内存可见性的方案:直接禁止编译器的优化,算得慢一点没有关系,主要目的实际结果和预期结果一样。

指令重排序

编译器会根据逻辑不变的条件下,对指令进行优化!

举个栗子:逍遥今天上班迟到了,老板在逍遥赶来上班的路上交给他三件事

  1. 去楼下取快递;
  2. 去超市买包烟(华子);

逍遥想着顺路,于是决定先买烟,再取快递。等逍遥刚把烟买完,老板想着交代给逍遥的任务是先去快递,后买烟,寻思着可能烟还没有买,于是发消息给逍遥说烟不用买了,只用取快递。这下烟也不能退,自己也不抽烟,老心疼了。
这就是指令重排序造成的后果

解决线程不安全的方法

需要解决的问题

结合线程不安全的原因,想到解决的办法那么需要

  • 保证原子性
  • 保证内存可见性
  • 禁止指令重排序

使用 synchronized 关键字

synchronized 关键字能够达到以上三个条件,也就是说使用 synchronized 关键字能够

  1. 保证原子性
  2. 保证内存可见性
  3. 禁止指令重排序

当使用 synchronized 修饰后,CPU 中会多两个指令 lock(上锁)和
unlock(释放锁),而在上面的例子( 多个线程修改同一个数据) 中的指令状态就会变成这个样子。
在这里插入图片描述
CPU 调度线程的时间仍然是不确定的,但是线程 B 会等待线程 A 的 unlock 结束后,再执行下面的 load、add、save 和 unlock 指令。
同时,当使用 synchronized 后,那么编译器不会对 指令优化(保证内存可见性) 以及 指令重排序

使用 volatile 关键字

volatile 关键字能够保证

  1. 保证内存可见性
  2. 禁止指令重排序

在上面的 内存可见性 中的例子,线程 A 进行修改数据,线程 B 在线程 A 修改数据还未写入内存的时候,就会导致线程 B 读取的数据还是未修改的。这样就导致了 bug。
因此在这中一个线程写、一个线程读的情况下,不需要保证线程的原子性,只需要保证内存可见性已经禁止指令重排序即可。 而volatile 关键字整好能够达到这样的要求。

为什么有 synchronized 关键字还需要 volatile 关键字呢?

synchronized 关键字由于保证了原子性,所以会导致效率特别慢,有些情况下我们只需要一个线程写、一个线程读,不需要保证原子性,那么就可以使用 轻量级 的 volatile 关键字来修饰,同时提高了效率。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/708236
推荐阅读
相关标签
  

闽ICP备14008679号