Java - 出现线程安全问题的原因有哪些?

一、线程安全问题是什么?

多线程被系统随机调度,使得进程的执行有多种可能性。其中某些可能性会造成进程的代码出现bug -> 线程不安全/安全问题。

举个例子:当两个线程尝试修改同一个变量的时候,每次的运行结果都不一样

创建两个线程, 让这俩线程同时并发的对一个变量自增 5w 次. 最终预期一共自增 10w 次.

//使用一个类来保存计数的变量
class Add{
    public int count;
    public void add(){
        count++;
    }
}
public class demo1 {
    static int Max=5_0000;
    static Add add=new Add();
    public static void main(String[] args) throws InterruptedException {
        //线程1 - 增加Max次
        Thread thread1=new Thread(()->{
            for(int i=0;i<Max;i++){
                add.add();
            }
        });
        //线程2 - 增加Max次
        Thread thread2=new Thread(()->{
            for(int i=0;i<Max;i++){
                add.add();
            }
        });
        thread1.start();
        thread2.start();

        //阻塞主线程,等待thread1,thread2运行结束再运行主线程的输出“和”语句
        thread1.join();
        thread2.join();

        //主线程阻塞解除,打印两个线程对变量count的累加效果
        System.out.println("和:"+add.count);
    }
}

运行结果:  

分析运行结果:

每次的运行结果都不一致,这是线程不安全造成的安全隐患。

分析线程不安全的原因:

从内存与cpu的角度来看:

一行count++代码其实对应三条指令:① 从内存中读取数据到cpu; ② 在cpu的寄存器中完成加法运算; ③ 将在寄存器得到的运行结果写回内存 

由于线程之间并发执行,两个线程,三条指令的执行步骤就有多种组合的可能性...

下图仅列举其中的几种可能性:

我们来画一下在各种可能性下,两个线程在cpu与内存之间处理指令的简略图:

 可能性1:线程2取的数据是线程1存储在内存中的结果,结果正确

可能性3:线程1,2执行任务,取得都是0,计算结果都为1,存回内存的都是1。结果错误

结论:

只有一个线程连续完成Load-Add-Save三个指令结束任务之后,另一个线程再出来开始执行任务Load-Add-Save才不会出错。否则不然。

二、出现线程安全问题的原因

1. 操作系统随机调度线程,导致线程之间抢占式执行任务

2. 个线程修改同一个变量

多个线程修改不同的变量,不会有影响;

多个线程读同一个变量,不会有影响

3. 多线程对同一个变量的修改操作不是"原子"的

原子指的是不可再拆分的微小单位

通过++来修改,修改操作对应三条指令,不是原子的;

通过=来修改,修改操作对应一条指令,视为原子的

上述例子中:

两个线程同时对一个变量进行++修改操作。由于++的修改操作并不是原子的,并且两个线程并发运行,就造成了线程1、2没办法在对方修改过的基础上进行修改而造成输出结果错误

4. 内存可见性引发的线程安全问题

常见场景是:a线程在写,b线程在读

a线程进行反复的读取/判断数据

读取数据:内存->寄存器(从内存读取数据到寄存器后,也可以直接从寄存器读取数据。

并且从内存读取数据的速度<<从寄存器读取数据的速度)

正常情况下:线程a进行读写和判断,线程2突然往内存写了新数据。线程b能够立即读到内存的变化,更新读到的数据

但是:进程运行的过程中,操作系统/JVM/编辑器可能会对这个过程进行优化

从内存中读取数据比较慢,反复读,每次读到的结果都一样。JVM就会进行优化 - 不再重复读,直接复用第一次从内存读到寄存器的数据 - 每次从寄存器读就好了

优化后,b线程往内存中写入了新数据,数据更新了

麻烦的事儿就在这!a线程经历了优化,没法读到内存中更新的数据感知不到变化。

内存可见性问题 - 内存数据改了,但是在优化的背景下,线程读不到/看不见

5. 指令重排序

有时候操作系统/编辑器/JVM为了提高效率,会交换指令之间的执行顺序

来看一个场景:Test t = new Test()

① 创建内存空间  ② 在内存空间上构造一个对象  ③把这个内存的引用赋值给t

正常情况下:

按②③的顺序,当第二个线程读到 t 为非null的时候,此时 t 一定是个有内存空间的有效对象

优化的背景下:(①肯定要先执行,但是②③的顺序可能会交换)

按③②的顺序,当第二个线程读到 t 为非null的时候,此时 t 仍然可能是个无效对象。因为还没有将新new出来的空间引用赋值给 t 时,就去读 t 的值了

🔊 为什么有时候交换顺序,可以提高效率呢?

举个例子:我们需要在超市买油-盐-米,但是超市里的柜台按米-油-盐的顺序排列。我们买油-盐-米的路程就要多于米-油-盐,后者交换顺序的效率明显就要高一些

有时候的优化能够给程序带来翻倍的效率提升,但有时候也会造成不好的影响!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值