前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程进阶一:从“并发引发的潜在问题”开始

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

作者头像
浩说编程
发布2021-08-17 17:23:45
1980
发布2021-08-17 17:23:45
举报
文章被收录于专栏:Java经验之谈

“并发编程”场景对于很多读者来说可以算是“既熟悉,又陌生”。

熟悉之处在于:对于一些有一定经验的读者,在面试过程中经常会被问到多线程、高并发的技术解决方案。

陌生之处则是我们的日常业务开发过程中很少用到。

所以本系列的初衷是帮助存在以上情况的读者填补并发编程这方面认知的空白,以便在需要的时候或者面试的时候能够有所帮助。

读者的收获

1、什么是并发编程

2、并发编程的潜在问题

1

CPU缓存引起的可见性问题

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

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

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

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

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

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

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

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

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

代码语言:javascript
复制
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缓存引起的可见性

2

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

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

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

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

代码语言:javascript
复制
count += 1;

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

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

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

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

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

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

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

3

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

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

编译之前:

代码语言:javascript
复制
int A = 1;
int B = 2;
int c = A + B;

编译之后:

代码语言:javascript
复制
int B = 2;
int A = 1;
int c = A + B;

这种顺序调整在并发的时候可能会造成意想不到的问题,比如下面这个例子:

代码语言:javascript
复制
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的成员变量的时候就可能引发空指针异常。

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 浩说编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档