反骨之硬件&软件为Java并发编程中挖的坑(可见性&原子性&有序性)

787 阅读5分钟

前言

本篇博文,主要集中并发编程的三个问题:可见性、原子性、有序性。

只讲理论,不谈如何解决。

说白了并发编程中的三个问题,就是先辈们给我们这些后辈留下来的坑!


那么,先辈们给并发编程带来什么坑?

相信大部分人都知道,一台计算机的组成包括但不限于:CPU、内存、显卡….

在这里, 笔者将计算机中的组抽象成几类,即:CPU、I/O设备、内存

有了以上三个分类,笔者接下来的文章就好写了。(滑稽)

  • Cpu多核缓存带来的数据----可见性问题(硬件):(建议直接跳到小节末尾, 看结论即可)

    讲道理,在处理速度方面CPU >> 内存 >> I/O设备

    这时候, 为了充分发挥CPU的计算速度, 硬件方面则是在CPU和内存之间, 增加了一种叫高速缓存的技术。

    总而言之,高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾

    当然, 在单核时代Cpu缓存并没有出现数据一致性问题, 但是在多核时代就不一样了。

    企业微信20190810021224.png

    在多核处理器中,由于Cpu并不与内存直接打交道, 而是通过高速缓存。

    同理, 内存也是并不直接与Cpu打交道,也是通过高速缓存与Cpu打交道。

    • cpu <——> 高速缓存 <———> 内存

    缓存一致性问题以及解决方案,网上一搜一大堆,笔者在此就不做过多的叙述。

    所以,只需要记住结论即可:

    Cpu缓存不一致为并发编程带来----可见性问题。(结论)


  • 操作系统中进程&线程上下文切换给并发编程带来操作----原子性问题:

    在以前的印象中,说到原子性,笔者张口就是:原子操作是一个不可分割的整体, 该整体中的所有指令,要么全部执行, 要么全部不执行, 没有中间状态。在此,笔者所写的原子性概念,仿佛过于宽泛和缥缈。而且,原子性在不同的使用场景,也有可能含义并不相同。

    接下来,我们试着从数据库事务并发编程两个方面来进行对比:

    在数据库中,原子性概念如下:

    • 事务被当做一个不可分割的整体,包含在其中的操作要么全部执行,要么全部不执行。且事务在执行过程中如果发生错误,会被回滚到事务开始前的状态,就像这个事务没有执行一样。

    在并发编程中,原子性概念如下:

    • 第一种理解:一个线程或进程在执行过程中,没有发生上下文切换
      • 上下文切换:指CPU从一个进程/线程切换到另外一个进程/线程(切换的前提就是获取CPU的使用权)。
    • 第二种理解:我们把一个线程中的一个或多个操作(不可分割的整体),在CPU执行过程中不被中断的特性,称为原子性。(执行过程中,一旦发生中断,就会发生上下文切换)

    从上文中可以看出,并发编程和数据库两者之间的原子性概念有些相似。

    都是强调,一个原子操作不能被打断!!

    所以,上下文切换给并发编程带来——原子性问题。(结论)


  • 编译器的编译优化给并发编程带来程序----有序性问题:

    讲道理,在计算机语言中,高级语言的运行都需要通过编译器转换成机器语言,通过机器语言在计算机上运行。

    那么,在编译器中为了提高运行速度,会对内存访问的有关操作(读&写)做一种优化,即指令的重排序。这种重排序在单线程环境下,不影响结果的正确性。但是,在多线程环境下可能会对结果的正确性产生影响。

    指令的重排序,是造成并发编程的有序性问题的原因。

    编译器带来指令重排序,而指令重排序造成有序性问题。

    编译器优化—带来—> 指令重排序—带来—>有序性问题

    所以,编译器的优化给并发编程带来—有序性问题!

总结

  • 多核Cpu的高速缓存为并发编程带来—可见性问题。
  • 线程的切换给并发编程带来—原子性问题。
  • 编译器给并发编程带来—有序性问题
  • 不管是可见、原子还是有序性,都是线程安全问题的表现形式。

最后,本篇博文,主要是对刚学习并发编程的朋友,提出并发编程的三个问题的概念。

说实话,笔者刚开始学的时候,并没有想过想过原子、有序、可见性的造成原因,直接统统死记硬背!

有时候想想,觉得自己挺可笑的。

总之,读者们要记住,整个Java并发包的设计与实现,都是为了解决并发编程的可见、原子、有序三个问题。

接下来,笔者会尝试着写写,Java是如何解决可见性、原子性和有序性的….