并发编程进阶一:从“并发引发的潜在问题”开始

“并发编程”场景对于很多读者来说可以算是“既熟悉,又陌生”。
熟悉之处在于:对于一些有一定经验的读者,在面试过程中经常会被问到多线程、高并发的技术解决方案。
陌生之处则是我们的日常业务开发过程中很少用到。
所以本系列的初衷是帮助存在以上情况的读者填补并发编程这方面认知的空白,以便在需要的时候或者面试的时候能够有所帮助。

读者的收获

1、什么是并发编程

2、并发编程的潜在问题

同名公众号:浩说编程 [ 技术分享 | 资源整合]

编辑消息:“阿里技术资料”,获取大厂技术资源,你会学到更多

一、CPU缓存引起的可见性问题

首先需要通过流程图来了解一下CPU处理数据的逻辑:

图片

可以看到,CPU在处理数据的时候涉及到三个区域:硬盘、内存、CPU中的缓存区

目标数据首先在硬盘中(数据库)通过I/O进入内存,然后再从内存进入CPU的缓存区,以供CPU处理。

CPU在处理之后会将数据暂时保存在自己的缓存中,在合适的时机再原路返回到硬盘中。

对于多核CPU来说,它的并发情况是这样的:

图片

参考上图,根据上面的内容:CPU在处理数据之后不会直接放回内存中。

所以对于同一个参数,每个CPU在将处理之后的数据放回内存之前,看到的都是各自缓存中的数据

也就是说参数在CPU之间是不可见的,这就导致了数据一致性的问题。

读者可以用下面这段代码来验证一下:

public class Test {
  private long count = 0;
  private void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
    }
  }
  public static long calc() {
    final Test test = new Test();
    // 创建两个线程,执行add()操作
    Thread th1 = new Thread(()->{
      test.add10K();
    });
    Thread th2 = new Thread(()->{
      test.add10K();
    });
    // 启动两个线程
    th1.start();
    th2.start();
    // 等待两个线程执行结束
    th1.join();
    th2.join();
    return count;
  }
}

代码中做了两个线程,调用了相同的代码:计算数值之和。

通过数学层面结果应该是20000,实际上结果并不等于20000,这就是并发编程的第一个问题:CPU缓存引起的可见性

二、线程切换引发的原子性问题

通过可见性问题相信读者对并发已经建立了一个初步的印象,接下来继续看并发存在的第二个问题:原子性

无论是编程语言Java还是CPU都支持多线程的方式执行多任务处理,而原子性问题就产生于两者切换线程的"最小命令单元"的差异。

图片

在Java中,这是一个最小命令单元:

count += 1;

受编程语言影响,读者可能会潜意识的认为在CPU中该命令同样也是最小命令单元,但其实不然。

在CPU中这个指令至少会被拆解成三个最小命令单元

1、把变量 count 从内存加载到 CPU 的寄存器

2、寄存器中执行 +1 操作

3、将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)

我们把Java的一条最小命令单元在CPU的多条最小命令单元执行的过程中不被中断的特性就叫做原子性

当这种原子性被破坏,就会发生原子性问题。

三、编译优化产生的有序性问题

并发的第三个问题是由编译器引起的,在我们的Java文件被编译成class文件的时候,编译器为了优化代码可能会在不影响最终结果的情况下,调整语句的顺序

编译之前:

int A = 1;
int B = 2;
int c = A + B;

编译之后:

int B = 2;
int A = 1;
int c = A + B;

这种顺序调整在并发的时候可能会造成意想不到的问题,比如下面这个例子,这是一个基于双重校验锁的单例模式实现 :

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

注意代码的第7行,编译器对于new操作进行了顺序优化:

图片

这个时候问题就出现了:

试想一下,A线程在执行了代码的第7行,顺序2之后,在执行顺序3之前切换到了B线程。

当B线程走到代码的第4行,由于A线程的顺序2使得变量instance已经有了指向,所以instance!=null,使得线程B直接跳到代码的第10行。

而此时并未执行顺序3对Singleton对象初始化,于是在我们调用instance的成员变量的时候就可能引发空指针异常。

这里逻辑可能会有点绕,读者可以多看几次理解一下。

以上就是并发产生的问题,之后的所有"并发编程"的内容都是为了解决这些问题而产生的,所以了解了问题根源对之后的学习会很有帮助。浩说编程,帮你学到更多。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值