别问我多线程为啥有BUG了

刚学习并发编程时候,肯定有这几个疑问:volatile是啥,为啥要用,我的代码怎么就不安全了。。。。。
从0开始,手把手教你,把你拉扯大,

操作系统的粗糙模型

cpu处理指令非常快,但是如果涉及到与内存交互,比如取一条数据,那么假设cpu直接向内存取值。cpu是填上一天,内存是地上一年。所以在cpu增加了缓存,cpu和缓存交互,缓存和内存交互。用来解决处理速度差异过大的问题。所以读到这,你得给我知道操作系统的粗糙模型是这样的。
在这里插入图片描述

内存可见性

单核时代,上面的粗糙模型正好使用,那么单核的多线程,就是一颗cpu的分时处理,所以不管你开多少个线程,都是一颗cpu,一片缓存,一个内存,多个线程之间读写数据对于cpu来说也是互相可见。 大概就是这样的, 可以理解为两个客人(线程)通过一个服务员(缓存)安排一个厨师(内存)做事。
在这里插入图片描述
所以可见性就是一个线程的操作其他线程都是可见的,就这么通俗易懂,主角是线程!
多核时代,问题就来了,每个cpu都有自己的缓存,就好比每个客人都有自己服务员。
在这里插入图片描述
这种情况不好好控制,厨师就疯了。

public class StackTest {
    private long count = 0;

    private void addMoney(){
        for (int i = 0; i < 10000; i++){
            count += 1;
        }
    }

    public long getCount(){
        return count;
    }
    public static void main(String[] args) throws Exception {
        StackTest stackTest = new StackTest();

        Thread thread1 = new Thread(() ->{
            stackTest.addMoney();
        });
        Thread thread2 = new Thread(() ->{
            stackTest.addMoney();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(stackTest.getCount());
    }
}

最后发现结果总是(10000,20000]。
这是因为多核cpu的多个缓存中都维护着一份count的变量。且多个缓存间不可见。那么就会出现:cpu1,cpu0最开始都缓存了count的值,都是0,然后都执行加1,两份缓存中的中值都变成了1,然后写回到内存后就是1,而不是我们期望的2。
这就是可见性导致的bug。

操作原子性

那就会有杠精问,老子就用单核的是不是就没问题了?
那我劝你再去学习一哈操作系统。算了,我接着拉扯你吧,首先要知道cpu的执行单位是指令,而不是高级语言中的一条语句。就比如上面的count += 1;需要三条指令才能完成。
1,将count变量从内存加载到cpu寄存器。
2,在寄存器中执行+1
3,写回到缓存。
乍一看没毛病,但是你还得知道,即便是单核下编写的多线程java,没有了多核并行还有单核的时间片切换!
在这里插入图片描述
就这么切换你能受得了?
所以cpu在执行一个或者一系列操作时候不被中断的特性叫做原子性,这就是原子性引发的bug

编译有序性

不要以为你写了个 int a = 1; int b = 2;处理器就一定会这么执行。我们写的高级语句,最后会被编译为计算机能够执行的指令,在这个过程,编译器会有一个优化阶段。可能会导致语句顺序发生变化。即:指令重排。
说这个你可能不懂,说双重检查锁定你就懂了,如果双重检查锁定都不懂,那就得拉扯你一把了。

public class SingleTon {
    static SingleTon instance;
    static SingleTon getInstance(){
        if (instance == null){
            synchronized (SingleTon.class){
              //  if (instance == null){
                    instance = new SingleTon();
             //}
            }
        }
        return instance;
    }
}

就这么几行代码。你是不是得问,指令重排在哪里发生了,才导致要写这么麻烦的代码。
是这样的:针对 instance = new SingleTon()。优化以后是这样的顺序,1,分配一块内存M。2,M的内存地址赋值给instance变量,3,在内存M上初始化对象。
来看流程图
在这里插入图片描述
看完上面的关键流程图,真的,以后别问我为啥多线程编程有bug了,喂什么吃什么

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值