多线程-内存模型

内存模型是深入了解多线程开发的基石

1.多线程起源
2.内存模型基础–硬件优化
3.内存模型详细说明
4.原子性
5.有序性
6.可见性
7.先行发生规则


1.多线程起源

计算机运行速度快,但是存储和通信子系统速度慢,导致cpu大部分时间是在等待存储设备读写操作,此时加入多线程可以提升程序性能。
多线程共享进程变量,如何对共享变量进行操作?

2.内存模型基础—硬件优化

在现代处理器和编译器中,为提升程序性能采取了很多措施。

  • 高速缓存:计算机存储设备与处理器运算速度差距太大,加入高速缓存作为处理器和存储器之间的缓冲,将运算需要的值从存储设备中复制到高速缓存中,运算结束后,再将运算结果同步回主内存中。
    这样导致一个问题,多处理器系统中,每个处理器都有自己的缓存,但它们又共享同一内存区域,导致数据缓存中数据不一致。
    为解决缓存不一致情况,定义了一系列协议,用于规范主存数据的读写。(为什么要说这个,因为内存模型有相似问题)(物理机的内存模型)
    这里写图片描述

  • 流水线

  • 指令重排序

    流水线和指令重排序会在后面详述。正是由于一系列的优化,在串行执行时不会出现的问题,在并发执行时可能会出现问题,所以需要定义一些规则来确保多线程程序执行的正确性。

3.内存模型详细说明

内存模型起因:平台无关性

java虚拟机定义内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,使java程序在各平台下都达到一致的访问效果。其他语言是直接利用物理硬件和操作系统的内存模型,不同的计算机其硬件和操作系统不一样,所以可能在一台计算机编译的程序,到另一台计算机上无法执行。

内存模型建立:充分利用硬件特性

为提升执行速度,充分利用硬件特性,模仿高速缓存,在java多线程中引入工作内存。java虚拟机规定所有变量都存储在主内存中,每个线程都有自己的工作内存(可与高速缓存类比),工作内存中保存线程所要使用的变量的副本,线程对变量的所有操作都是在工作内存中进行的,线程间交互是通过主内存进行。
这里写图片描述
主内存,工作内存对应关系:

主内存对应堆,存放共享变量
工作内存对应线程栈,一个线程只能读取自己的线程栈,包含当前方法的所有变量信息。

如果方法中包含本地变量是:

  1. 基本数据类型,将直接存储到工作内存的栈帧结构中。
  2. 引用类型,变量引用存储在工作内存栈帧结构中,对象实例存储在主内存中

实例对象的成员变量,不管是基本类型还是引用类型都存储在主内存中。如果实例对象被多线程共享,倘若两个线程调用同一对象的同一方法,两个线程将要操作的数据拷贝到自己的工作内存中,执行完操作后才刷新到主内存。

主内存,工作内存交互方式:

在上述结构中,必然存在数据不一致问题,所以java内存模型定义了访问变量的规则,限定主内存和工作内存之间的交互方式。
定义了8个操作:(虚拟机实现时必须保证每一种操作都是原子性的)

  1. lock:作用于主内存变量,把一个变量标识为一个线程独占状态,当一个变量被标识为lock状态,清除工作内存。
  2. unlock:作用于主内存变量,解除锁定。
  3. read:作用与主内存变量,将变量从主内存读取到工作内存。
  4. load:作用于工作内存变量,将read的变量载入到工作内存副本中。
  5. use:将工作变量的副本传递给执行引擎。
  6. assign:将执行引擎接收的值赋值给工作变量副本
  7. store:作用于工作内存,将工作内存变量值传递给主内存
  8. 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.可见性:

当一个线程修改了共享变量,其他线程能够立即知晓这个修改。

确保措施:

  1. volatile关键字:在变量修改后同步回主内存,在变量读取前从主内存刷新新的变量值。
  2. synchronized关键字:对一个变量执行unlock操作之前,必须先将变量同步回主内存,执行lock前,清楚工作内存中该变量
  3. final关键字

7.先行发生规则:

如果在内存模型中所有有序性仅仅靠volatile和synchronized关键字解决,有些操作将变得很繁琐。

先行发生原则(判断数据是否存在竞争,线程是否安全):

java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,操作A产生的影响被B观察到,影响包括修改内存中共享变量值,发送了消息,调用了方法。

内存模型中存在天然的先行关系,如果不在以下说明的情况中,虚拟机会对其进行随意重排序。

  1. 程序次序规则:保证语义串行性,如果前后存在依赖关系,不能进行重排序,比如int a = 1; int b = a+1;
  2. volatile规则:对一个volatile变量的写操作先行发生于读操作
  3. 锁规则:一个unlock操作先行发生于随后的加锁操作
  4. 线程启动规则:start方法先于其后每一个操作
  5. 线程终止规则
  6. 线j程中断规则
  7. 对象终结规则:一个对象的初始化完成先行发生于其finalize方法开始
  8. 传递性:A先于B,B先于C,则A先于C。

总结:java内存模型是为了实现平台无关性,同时又充分利用硬件特性,建立了一套内存访问结构,规则。通过规则,定义工作内存、主内存之间的数据交互方式。在指令重排序等优化方式情况下,导致多线程可能会出现问题,通过采取一系列措施,确保原子性、有序性、可见性。

猜你喜欢

转载自blog.csdn.net/demodan/article/details/80541568