简介
本文是并发编程的第二篇,主要介绍并发编程过程中必须要注意的三个特性:原子性、可见性、有序性。
1. 为什么多线程会存在并发问题?
下图是JMM内存模型,根据JMM规范每个线程都有自己的本地内存,本地内存是线程私有的,线程之间的数据交互必须通过主内存,在对共享变量进行操作时会先把共享变量复制到本地内存,在本地内存操作完成之后再回写到主内存中,这样当两个线程同时操作一个共享变量的时候就会就会出现线程安全问题。
所以我们要避免线程安全,就要保证线程的以下三个特性:
1.1. 原子性
原子性就是不可拆分,一个操作或者多个操作,必须要全部执行并且执行的过程不会被任何因素影响,比如上图中线程A操作一个共享变量,在线程还没把操作结果回写到主内存前线程B也对共享变量进行读写,那这个操作就不具备原子性。
所以,保证原子性的办法就是加锁,让操作共享变量的操作串行化执行。
1.2. 有序性
CPU 在执行指令时,底层会对指令在不影响最终结果的原则下进行优化重排序,并不一定按照代码的顺序执行。
比如我们 执行Object obj = new Object()
这个操作的指令步骤如下:
- 申请内存,new 一个对象,成员变量给默认值(这个状态就是半初始化状态)
- 对象初始化,按照编码设定值
- 把对象地址和引用变量建立连接
但是完全有可能 2 和 3 指令互换位置,让this 指向一个半初始化状态的对象,这就是this对象逸出。
保证有序性的办法就是使用volatile
关键字来修饰变量,被 volatile
修饰的变量CPU不会进行执行顺序优化。
1.3. 可见性
可见性就是当一个变量被一个线程修改后,另一个线程能够立刻发现,从主存值中读取最新的值,而不是继续使用自己本地内存未修改之前的值,这就是可见性。
- 使用 volatile 修饰的变量可保证可见性
- synchronizer也会触发内存缓存同步刷新,保证共享变量的可见性。
以上就是高并发编程的三个特性了,后面文章我会详细介绍 synchronizer
、volatile
以及各种锁的原理和用法。
2. 几个常见问题的简单解答
- 缓存一致性协议MESI是什么?
- 单核CPU会存在线程安全问题吗?
- 底层硬件CPU结构、内存条、及我们常说的Java 运行时数据区(堆、栈、方法区等)和JMM内存模型之间的对应关系是什么?
接下来看图说话:
计算机中CPU的运行速度远远大于内存的运行速度,所以为了高效利用CPU资源,在CPU和主内存之间加入了高速缓存的结构,所以这里就有点类似JMM的内存模型了,当各个CPU核心同时缓存壁并操作同一个变量时就会出现并发问题了(这个也是在有些文章中会介绍的线程安全问题的原因),那CPU是如何解决的呢?所以在多核系统中为了保证各个Cache的一致性CPU通过总线锁和缓存一致性协议保证各个缓存的一致。这个就是缓存一致性协议。
那么问题来了,既然有了缓存一致性协议,为什么还会出现并发问题?
思考下,我们先不说缓存一致性的实现细节方案,但是既然要保证一致性,那肯定得在缓存被更新的时候要通知其他缓存,修改的时候要加锁,这就引发了锁和通信成本带来的CPU性能下降问题。为了优化这个问题,CPU采用了一个叫存储缓存(Store Buffers)的东西,它的作用就算CPU在执行计算任务的时候可以把需要解决缓存一致的数据先存起来,先执行主要计算任务,等特定条件再解决缓存问题,最后保证缓存是一致的就可以,这样性能是得到了优化,但是缓存在某一刻可能还是会出现不一致的问题,所以还是会有线程安全问题。
那单核CPU还会有线程安全问题吗?答案是有的,因为我们文章开头讲的JMM内存模型决定的线程安全原因。接下来说下他们的关系:
CPU结构它是存在于CPU上的物理结构,Java 运行时数据区(堆、栈、方法区等)它是存在于我们的主内存(内存条)上,然后进行的一个逻辑划分,JMM内存模型他是对内存的一个更高层次的抽象,它是屏蔽了底层硬件的差异,使得Java应用程序可以在不同环境运行,如果底层CPU是多核的,那么JMM允许使用CPU的缓存做JMM中线程私有内存的实现。设想如果单核没有线程安全问题,多核有线程安全问题,那就不符合Java跨平台设计初衷了。JMM模型 和Java运行时数据区他们两个是从不同的角度去说的,没有必然的对应关系,如果说非得有对应关系,那可以这样去理解,JMM中线程私有内存对应运行时的部分栈内存,JMM中的主内存对应堆区,这就算它们之间的关系啦。
为了突出本文并发编程的特性,这里相关的知识就不再展开介绍了,后面我会专门抽出章节详解这块内容。