出现线程安全问题,一般是因为主存和工作内存数据不一致以及重排序,那今天就说一下这两个方面。
乱序执行优化
乱序执行优化是多核CPU为了提高效率而做的不符合代码规则的优化。
int a = 1;
int b = 1;
int c = a + b;
- 正常我们看到的执行顺序是A-B-C,但是因为CPU的重排序,运行顺序有可能变为B-A-C,这时候结果是不会受到任何影响的。
JMM
-
说到并发就要设计多个线程之间是如何通信的,通信可以分为两种:消息传递以及内存共享,而java主要使用到内存共享。说到内存共享就要先来看一下java的内存结构了。
- Java的内存结构主要分为栈和堆。
- 栈
- 栈的速度仅次于寄存器,速度快,但是大小和生存周期是固定的
- 主要用来存放一些基本类型的局部变量。
- 不同的线程栈之间是相互独立的,存放的变量对于其他线程不可见,即使他们运行的是同一段代码。
- 堆
- 是运行的数据区,是动态分配的(比如new关键字),所以速度存取速度较慢
- 存在垃圾回收,所以大小和生命周期是不确定的。
- 是共享的,主要用来存放对象
- 当一个线程栈持有一个对象,他只是持有了这个对象的引用(内存地址),真正的对象还是在堆上。
- 一个对象的成员变量,无论是啥类型,都是跟着对象存在于堆上的。
- 这时候就有个问题,如果多个线程,同时访问同一个对象的同一个方法,每个方法内又有局部变量。会发生啥呢。
- 其实啥也不会发生,多个线程访问同一个方法,他们其实只是持有方法内局部变量的拷贝。
硬件结构和JMM的关系
- 如果要从内存中读取数据处理后写回,都经历了什么(从硬件角度来看)
- 首先cpu发出指令
- cache会在合适的时机把数据从内存读过来
- cpu寄存器去执行计算操作啥的
- 寄存器把数据写回到cache
- cache在合适的时机把数据(上一个博客中提到的缓存行)写回主存\
- 硬件结构和JMM有啥关系呢
- 咱们上面说的栈和堆是在主存里的~当然,也会在不同的时机存在于cpu的缓存和寄存器(寄存器的速度比缓存还要快)
硬件结构和JMM更抽象的关系
- 前面说啦,java中线程的通信是通过内存的共享,那如果要完成一次通信,要经过这两步
- 线程1从主存中把变量读到了本地内存,进行一系列操作后,写回到主存
- 线程2把数据从主存中读到本地内存
- 这里说的本地内存是一个抽象的概念,他包括了上面说的,cache/寄存器啥的。
- 那线程安全为啥会产生呢,就是因为这个操作(基本上线程安全问题都是性能优化导致的)
- 如果一个线程要使用一个共享变量,首先要读到本地内存,后续的读写操作都是作用于本地内存上的副本的,执行完一系列操作后某个时刻,这个共享变量才会被从本地内存写回到主存。
- 注意,在写回到主存前,对于数据所执行的操作对于别的线程是不可见的。
- 如果此时另一个线程读取了主存中的共享变量,就产生了“脏读“。可以使用关键字”volatile“来解决,这个后续会详细说。
- 内存可见性JMM是通过happens-before关系来提供的。这个在参考资料有详细说,后面用到的时候也会写出来。
接下来干啥
- 明天说一下同步操作和同步规则后,理论的知识就暂时到这里。接下来会说一下并发模拟用到的工具以及代码。
更详细的参考资料:
http://ifeve.com/java-memory-model-6/
https://juejin.im/post/5ae6d309518825673123fd0e