线程安全问题的原因

目录

线程不安全会发生什么

线程安全发生的原因

1.抢占式执行()

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

3.修改操作不是原子的

4.内存可见性

5.指令重排序


线程不安全会发生什么

首先,我们先来看看这段代码:

class Counter{
    volatile int count;
    public Counter(){
        this.count = 0;
    }

    public void add(){
        count++;
    }

    public int get(){
        return count;
    }
}

//创建两个线程,分别让count自增10000次
public class ThreadDemo9 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++){
                counter.add();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}

本以为结果会是2w,但每次运行的结果都不一样,这就是线程不安全导致的结果

线程安全发生的原因

为啥会出现上述情况,和线程的调度随机性密切相关

count++操作,本质上是三个cpu指令构成:

1.load,把内存中的数据读取到cpu寄存器中.

2.add,将寄存器中的值,进行+1运算

3.save,把寄存器中的值写回到内存中

当两个或多个线程进行操作的时候,顺序会有很多种变化

例如:

 如果是这样的话,那两个线程之间互不冲突,可以得到正确的值

 但如果是上述情况的话,t1不知道t2将count的值进行了改变;

所以t1最后返回的值只是自增了一次的count

总结下来,线程不安全的原因有以下几点

1.抢占式执行()

由于随机调度的原因,线程中的代码执行到任意一行,都随时可能会被切换出去,这样就可能导致在切换出去后切换回来的过程中值发生修改

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

在一个线程在对一个数据进行修改时,其他线程也进行了修改,这个线程可能无法及时更新这个数据

这是从宏观角度来看

3.修改操作不是原子的

原子是不可分割的最小单位

像上述++操作,里面可以拆分为三个操作 load,add,save,所以不是原子的,在进行某个操作的时候cpu随时可能会去执行别的操作

而某个操作对应单个cpu指令,就是原子的

但这个操作对应多个cpu指令,大概率就不是原子的了

这个就是从微观角度来看

4.内存可见性

这个原因和上述案例不同

我们先写出一个bug来:

public class ThreadDemo {
    public static boolean bool = true;
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Thread t1 = new Thread(() -> {
            while(bool){

            }
            System.out.println("t1结束");
        });
        t1.start();

        Scanner scanner = new Scanner(System.in);
        bool = scanner.nextBoolean();

    }
}

上述代码中,当我们从键盘输入false的时候,t1应该输出"t1结束",然后结束线程;

但我们输入false后,t1并没有结束.

原因就是内存可见性的问题:

t1在读取bool的时候,需要从内存上load到cpu寄存器上,然后进行compare;

虽然读取内存的速度很快,但读取寄存器的速度更快;

所以在频繁的读取下,编译器就做了一个大胆的决定:

将load指令省略,直接从编译器里读;

这就造成了bug产生的原因

5.指令重排序

指令重排序是编译器优化的策略

它会调整代码执行的顺序,让程序更高效,前提是整体的逻辑保持不变

在单线程下容易保证,但是在多线程的情况下就不好说了

例如:

class Student{
    public void learn(){
        System.out.println("学习");
    }
    public void eat(){
        System.out.println("吃饭");
    }
}

public class ThreadDemo {
    public static Student student;

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //t1进行初始化
        Thread t1 = new Thread(() -> {
            student = new Student();
        });
        t1.start();
        
        //t2进行判断
        Thread t2 = new Thread(() -> {
            if(student != null){
                student.learn();
                student.eat();
            }
        });
        t2.start();
    }
}

在t1进行初始化的时候大体可以分成三个步骤

1.申请内存空间

2.调用构造方法(初始化内存的数据)

3.把对象的引用赋值给student(内存地址的赋值)

这个时候可能就会发生指令重排序:

步骤1肯定是先执行,但步骤2和步骤3的先后顺序就是未知的

如果是在单线程里,步骤2和3的顺序变化不会有什么影响;

但在多线程里面就会因为指令重排序出现问题:

假设步骤1执行完后先执行的步骤3,但此时t2开始启动:

由于t1的步骤3已经完成,student已经是非空了,但student还没有被成功初始化,这个内存里什么都没有

此时调用student的learn和eat方法就不知道会发生什么,很可能就会产生bug

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值