专栏首页洁癖是一只狗并发编程问题为什么都很诡异

并发编程问题为什么都很诡异

并发编程对于很多人说都是比较难的,总是出现一些莫名其妙的bug,让我们很是苦恼,那么他到底是难在哪里呢,今天就带大家看看引起并发bug的根源

缓存引起的可见性问题

在单核时代,所有的线程都操作在一个cpu上执行,因此cpu缓存和内存的可见性很容易解决,如下图,线程A对变量V的改变之后,线程B是可以之间看到的,因此不存在可见性问题

但是在现如今的多cpu时代,往往就不那么容易了,如下图,当线程A修改了变量V之后,对于线程B是不可见的,两个线程各自操作不同的CPU缓存,比如,初始值V的值为0,当线程A修改了V=V+1,此时CPU1里面的变量的V的值是1,同时同步到主内存中,此时线程B获取到CPU2缓存的值是V=0,此时就会引起数据不一致,为什么线程A已经修改了V的值,而线程B看到的值还是V=0,这个就是可见性的问题

线程切换到时的原子性问题

在早期的时候,由于IO性能太差,就引进了多进程的操作,正如操作系统允许某个程序执行一小段时间,比如50毫秒,过了50毫秒就会执行另外一段程序,这里的50毫秒就是我们说的时间分。

在一个时间片中,如果一个进程执行IO操作,此时就可以把CPU让出来,让CPU执行其他程序,当到上一个Io读取操作完成,再次唤醒进程,就可以再次获取CPU的执行权限.由于早期这种多进程是不进行共享内存的,因此各自执行各自的互不影响,任务的切换仅仅切换内存映射地址,而现在的并发编程中,我们使用的多线程,进行任务调度,多线程任务切换成本比较低,但是多个线程之间是共享内存的,因此会带来一系类问题.这也是并发编程出现问题的源头之一.

正如我们在代码中写count+=1操作,他的完成并不是一条指令完成的,会分成三步,

  1. 首先,先要把内存的count加载到CPU寄存器中
  2. 然后在CPU寄存器中计算加1
  3. 然后同步到内存中

如上操作,在操作系统中,我们执行代码,一句代码并不是真正的一条指令,可能分了很多指令,如下图,线程A和线程B 同时执行代码count+=1,由于线程切换导致计算错误,我们期望的是2,但是实际上计算出来的值却是1.

线程切换的发生可以在count+=1之前也可以在之后,但是不可能发生在中间,我们往往把一个或多个操作在CPU指令中不被中断的特性叫做原子性,但是我们潜意识中任务一句代码就是一个原子操作,因此就会造成我们意想不到的问题。

编译优化带来的有序性问题

我们程序中写的代码顺序往往并不是真正执行的顺序,如下声明变量

int a=7
int b=6

在代码编译之后的顺序就是下面这种

int b=6
int a=7

上面虽然顺序改变了,其实是并不影响结果,但是这种编译优化也会带来我们一向不到的问题,如下面代码,经典的单例模式代码

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

正常单线程并不会发生什么问题,但是在多线程中,就会产生意想不到的问题,正如两个线程A和B,同时调用getInstance(),两个线程同时到了第一个instance=null.且同时满足,然后到了synchroized中,此时只有一个线程能够获取到锁,假设是线程A获取到,然后线程A实例化instance,在释放锁,此时线程B获取到锁,然后发现实例instance已经不为null,就会直接返回,整体流程很是完美,实际上是有很大的问题,

首先,我们看一下new一个对象的正常过程

  1. 分配一块内存M
  2. 把内存M中初始化Singleton
  3. 把内存地址M复制给instance变量

但是经过编译优化之后,他的流程可能就是下面这样

  1. 分配一块内存M
  2. 把内存地址M复制给instance变量
  3. 把内存M中初始化Singleton

如果是优化后的流程,会引起什么问题呢,当我们的线程A在执行完第二步的时候,线程切换到了线程B,此时instance!=null,线程直接返回线程A创建的实例,但是此时的实例并没有进行初始化,因此在后面我们使用实例的属性的时候,就可能导致空指针。

最后我们把正确的单例模式的代码切出来(仅仅是在变量上加了一个volatile)

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

下次我们继续讲解有什么办法避免上面的问题,如果文章对你有一丝丝帮助麻烦点击关注,也欢迎转发或点赞,谢谢

本文分享自微信公众号 - 洁癖是一只狗(rookie-dog),作者:洁癖汪

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-10-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 如何解决可见性,有序性,原子性

    上一次我们说到了可见性,原子性,有序性,今天我们看看如何解决这个问题,今天我们先看看可见性和有序性,因此我们先要知道java内存模型

    小土豆Yuki
  • Mysql数据--死锁解密

    Mysql行锁是在引擎中实现的,并不是所有的存储引擎都支持行锁,比如myisam就不支持行锁,而innodb支持行锁,myisam在并发度高的系统中就会影响系统...

    小土豆Yuki
  • Mysql事务隔离级别

    在所有事物中可以看到事物没有提交的结果,实际应用中是很少的,他的性能也不比其他隔离级别好很多,读到未提交的结果导致脏读

    小土豆Yuki
  • Java 开发, volatile 你必须了解一下

    古时的风筝
  • 设计之禅——单例模式详解

    有时候我们只需要一个类只有一个对象,如,线程池、缓存、windows的任务管理器、注册表等,因此就有了单例模式,确保了一个类只存在一个实例。单例模式的实现非常简...

    夜勿语
  • ReentrantLock可重入锁 Krains 2020-08-27

    与 synchronized 一样,都支持可重入,但相对于 synchronized 它还具备如下特点

    Krains
  • 关于浏览器环境JS单线程及小程序双线程架构的一点思考

    1.通过了解浏览器线程的一些知识我们知道浏览器进程中GUI线程是与JS引擎线程互斥的。 2.小程序的架构是JsCore执行js逻辑代码+webview页面渲染...

    薛定喵君
  • 阿里一道Java并发面试题 (详细分析篇)

    我个人一直认为:网络、并发相关的知识,相对其他一些编程知识点更难一些,主要是不好调试并且涉及内容太多 !

    美的让人心动
  • nginx location配置

    location在nginx中起着重要作用,对nginx接收到的请求字符串进行处理,如地址定向、数据缓存、应答控制、代理转发等 location语法 locat...

    dys
  • MySQL死锁问题定位思路

    show status like ‘innodb_row_lock%'; 从系统启动到现在的数据

    用户1272933

扫码关注云+社区

领取腾讯云代金券