内存模型是深入了解多线程开发的基石
1.多线程起源
2.内存模型基础–硬件优化
3.内存模型详细说明
4.原子性
5.有序性
6.可见性
7.先行发生规则
1.多线程起源
计算机运行速度快,但是存储和通信子系统速度慢,导致cpu大部分时间是在等待存储设备读写操作,此时加入多线程可以提升程序性能。
多线程共享进程变量,如何对共享变量进行操作?
2.内存模型基础—硬件优化
在现代处理器和编译器中,为提升程序性能采取了很多措施。
高速缓存:计算机存储设备与处理器运算速度差距太大,加入高速缓存作为处理器和存储器之间的缓冲,将运算需要的值从存储设备中复制到高速缓存中,运算结束后,再将运算结果同步回主内存中。
这样导致一个问题,多处理器系统中,每个处理器都有自己的缓存,但它们又共享同一内存区域,导致数据缓存中数据不一致。
为解决缓存不一致情况,定义了一系列协议,用于规范主存数据的读写。(为什么要说这个,因为内存模型有相似问题)(物理机的内存模型)
流水线
指令重排序
流水线和指令重排序会在后面详述。正是由于一系列的优化,在串行执行时不会出现的问题,在并发执行时可能会出现问题,所以需要定义一些规则来确保多线程程序执行的正确性。
3.内存模型详细说明
内存模型起因:平台无关性
java虚拟机定义内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,使java程序在各平台下都达到一致的访问效果。其他语言是直接利用物理硬件和操作系统的内存模型,不同的计算机其硬件和操作系统不一样,所以可能在一台计算机编译的程序,到另一台计算机上无法执行。
内存模型建立:充分利用硬件特性
为提升执行速度,充分利用硬件特性,模仿高速缓存,在java多线程中引入工作内存。java虚拟机规定所有变量都存储在主内存中,每个线程都有自己的工作内存(可与高速缓存类比),工作内存中保存线程所要使用的变量的副本,线程对变量的所有操作都是在工作内存中进行的,线程间交互是通过主内存进行。
主内存,工作内存对应关系:
主内存对应堆,存放共享变量
工作内存对应线程栈,一个线程只能读取自己的线程栈,包含当前方法的所有变量信息。
如果方法中包含本地变量是:
- 基本数据类型,将直接存储到工作内存的栈帧结构中。
- 引用类型,变量引用存储在工作内存栈帧结构中,对象实例存储在主内存中
实例对象的成员变量,不管是基本类型还是引用类型都存储在主内存中。如果实例对象被多线程共享,倘若两个线程调用同一对象的同一方法,两个线程将要操作的数据拷贝到自己的工作内存中,执行完操作后才刷新到主内存。
主内存,工作内存交互方式:
在上述结构中,必然存在数据不一致问题,所以java内存模型定义了访问变量的规则,限定主内存和工作内存之间的交互方式。
定义了8个操作:(虚拟机实现时必须保证每一种操作都是原子性的)
- lock:作用于主内存变量,把一个变量标识为一个线程独占状态,当一个变量被标识为lock状态,清除工作内存。
- unlock:作用于主内存变量,解除锁定。
- read:作用与主内存变量,将变量从主内存读取到工作内存。
- load:作用于工作内存变量,将read的变量载入到工作内存副本中。
- use:将工作变量的副本传递给执行引擎。
- assign:将执行引擎接收的值赋值给工作变量副本
- store:作用于工作内存,将工作内存变量值传递给主内存
- write:作用于主内存,将store的值写会到主内存变量中。
定义这些原子操作是为了确保在并发情况下,内存访问的安全性。但是存在例外,对于64位的long、double基本类型数据,在没有被vilatile修饰情况下,允许为非原子性操作。
对于volatile变量将在后期内存可见性中详述。
4.原子性:
java内存模型是围绕原子性、可见性、有序性进行,主要为了确保此三特征成立而采取的一系列措施,比如vilotaile、synchronized。
原子性定义:一个操作要么全部执行,要么完全不执行。在内存模型中,定了了6个操作read、load、use、assign、store、write对基本数据类型(除64位long、double)都是原子操作。
如果需要大范围的原子操作,可以使用lock、unlock操作进行,对应于代码中的synchronized关键字。
之所以要确保原子性,是为了避免脏读等情况发生。
5.有序性:
因为指令重排序的原因(为了提升代码执行速度,减少流水线中空节拍),计算机执行代码的顺序与代码的书写顺序可能不一样。
所以在多线程环境下,由于重排序的影响,可能会导致程序执行结果与期待结果不一致,所以在多线程环境下要控制重排序影响。
/**
*可能会出现将flag和a进行重排
*/
class MixedOrder{
int a = 0;
boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void read(){
if(flag){
int i = a + 1;
}
}
}
内存模型为确保有序性:
- volatile关键字:禁止指令重排序
- synchronized关键字:同一个锁的两个同步块只能串行进入。
指令重排序:
为提升程序执行效率,采用流水线操作,将一条指令划分为不同步骤,每一个步骤所要使用的硬件不一样。如下:
1. 取指IF
2. 译码或取寄存器操作数ID
3. 执行或有效地址计算EX
4. 存储器访问MEM
5. 写回WB
(以下图片来自http://blog.csdn.net/javazejian/article/details/72772461,里面对于指令重排讲解很详细)
进行指令重排可以减少空节拍,提升速度,但是在多线程编程中,出现重排会出现问题。
6.可见性:
当一个线程修改了共享变量,其他线程能够立即知晓这个修改。
确保措施:
- volatile关键字:在变量修改后同步回主内存,在变量读取前从主内存刷新新的变量值。
- synchronized关键字:对一个变量执行unlock操作之前,必须先将变量同步回主内存,执行lock前,清楚工作内存中该变量
- final关键字
7.先行发生规则:
如果在内存模型中所有有序性仅仅靠volatile和synchronized关键字解决,有些操作将变得很繁琐。
先行发生原则(判断数据是否存在竞争,线程是否安全):
java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,操作A产生的影响被B观察到,影响包括修改内存中共享变量值,发送了消息,调用了方法。
内存模型中存在天然的先行关系,如果不在以下说明的情况中,虚拟机会对其进行随意重排序。
- 程序次序规则:保证语义串行性,如果前后存在依赖关系,不能进行重排序,比如int a = 1; int b = a+1;
- volatile规则:对一个volatile变量的写操作先行发生于读操作
- 锁规则:一个unlock操作先行发生于随后的加锁操作
- 线程启动规则:start方法先于其后每一个操作
- 线程终止规则
- 线j程中断规则
- 对象终结规则:一个对象的初始化完成先行发生于其finalize方法开始
- 传递性:A先于B,B先于C,则A先于C。
总结:java内存模型是为了实现平台无关性,同时又充分利用硬件特性,建立了一套内存访问结构,规则。通过规则,定义工作内存、主内存之间的数据交互方式。在指令重排序等优化方式情况下,导致多线程可能会出现问题,通过采取一系列措施,确保原子性、有序性、可见性。