Skip to content

Latest commit

 

History

History
205 lines (100 loc) · 8.95 KB

Java内存模型.md

File metadata and controls

205 lines (100 loc) · 8.95 KB

Java内存模型

一. 基础(硬件效率的一致性):

1. 计算机内存模型:

为了提高计算能力,减少计算机存储设备与处理器运算速度的差距,现代计算机系统在内存处理器之间加入了一层读写速度与处理器运算速度尽可能接近的高速缓存(Cache)作为内存与处理器之间的缓冲.
将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存中,这样处理器就无须等待缓慢的内存读写.

处理器,高速缓存,主内存间的关系

  • 这种模型虽然很好的解决了处理器与内存的速度矛盾,但却引入了缓存一致性(Cahce Coherence)的问题:

多处理器系统中,每个处理器都有自己的高速缓存,它们共享同一块主存(Main Memory).当多个处理器的运算都涉及同一块主内存区域时,可能导致各自的缓存数据不一致.

  • 为了解决一致性问题,需要各个处理器访问缓存时都遵循一致性协议,读写时根据协议进行操作

2. 处理器指令重排序:

为了使处理器内部单元能被充分利用,处理器可能会对代码进行乱序执行(Out-Out-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证结果与顺序执行结果一致,但并不保证与输入代码的顺序一致.

  • Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化.

二. Java内存模型:

1. 内存模型中的关系(可与计算机内存交互关系类比):

  1. Java规定了所有的变量(静态字段,实例字段构成数组对象的元素)都存储在主内存中.

  2. 每条线程拥有自己的工作内存,其中保存了该线程使用到的主内存副本拷贝.

  3. 线程对变量的所有操作(赋值,读取等)都必须在自己的工作内存中进行,而不能直接读写主内存中的变量.不同的线程之间也无法直接访问对方工作内存中的变量.
    线程间的变量值传递均需要通过主内存来完成.

    线程,工作内存和主内存交互关系

  • 主内存主要对应于Java堆中的对象实例数据部分,工作内存则对应于虚拟机栈中的部分区域.

    从更低的层次上说,主内存直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器高速缓存中.

2. 内存间交互操作:

  1. Java内存模型定义了8种操作(原子,不可再分)来完成主内存与工作内存之间的具体交互细节:

    • lock(锁定): 作用于主内存变量,把一个变量标识为一条线程独占状态.

    • unlock(解锁): 作用于主内存变量,把处于锁状态的变量释放出来.

    • read(读取): 作用于主内存变量,把变量值从主内存传到线程的工作内存,以便load动作使用.

    • load(载入): 作用于工作内存变量,把read操作从主内存得到的变量放入工作内存变量副本中.

    • use(使用): 作用于工作内存变量,把变量值传递给执行引擎.当虚拟机遇到需要使用变量值的字节码指令时会执行这个操作.

    • assign(赋值): 作用于工作内存变量,把执行引擎的值,赋给工作内存变量.虚拟机遇到赋值的字节码指令执行此操作.

    • store(储存): 作用于工作内存变量,把工作内存变量的值传送到主内存中.

    • write(写入): 作用于工作内存变量,把store操作从工作内存得到的变量的值放入主内存变量中.

  • 把变量从主内存复制到工作内存: 顺序执行readload.
  • 把变量从工作内存同步回主内存: 顺序执行storewrite.
    (顺序执行,不保证连续执行,可插入其他指令)
  1. Java内存模型规定执行上必须满足以下规则:

    1. 不允许readload,storewrite操作之一单独出现.

    2. 不允许一个线程丢弃它的assign操作.

    3. 不允许线程无原因的(未发送assign)把数据从工作内存同步回主内存中.

    4. 新的变量只能在主内存中诞生.

    5. 一个变量同一时刻只允许一个线程对其进行lock操作,lock可被同个线程重复执行.

    6. 对一个变量进行lock操作,工作内存会清空此变量值,执行引擎使用这个变量之前,需重新执行load(主内存到工作内存)或者assign(执行引擎到工作内存)初始化变量值.

    7. 不允许unlock被其他线程lock或未被lock的变量.

    8. 对一个变量unlock之前,必须先同步(执行store,write)回主内存中.

  2. volatile的特殊规则(有空另开一篇详细整理下):

    volatile是Java虚拟机提供的最轻量级同步机制.

  • 两种特性:

    1. 可见性: 保证volatile修饰的变量对所有线程可见.

      • volatile变量只能保证可见性,只能再一下场景使用:

        1. 运算结果不依赖当前值,或者能保证只有单一线程修改变量值.

        2. 变量不需要与其他的状态变量共同参与不变约束.

    2. 禁止重排序优化: 普通变量仅仅会保证该线程方法的执行过程中所依赖的赋值结果的地方都能获得正确结果.volatile能保证变量赋值操作顺序与代码中的执行一致.

      • 内存屏障(Memory Barrier/Memory Fence): 指重排序时不能把后面的指令重排序到内存屏障之前的位置.
  • volatile变量读操作性能消耗与普通变量几乎没差别,写操作开销比较大.

  • 假设T标识一个线程,V和W分别表示两个volatile型变量,在进行read,load,use,assign,storewrite时需要满足如下规则:

    1. 每次使用V前都必须从主内存刷新最新的值,保证能看见其他线程对变量V的最后修改.

    2. 工作内存中,每次修改V后都必须立刻同步回主内存中,保证其他线程可以看到自己对变量V的修改.

    3. 要求volatile修饰的变量不会被指令重排序优化,保证代码执行顺序程序的顺序相同.

    4. 对于long,double型变量的特殊规则:

      • Java内存模型允许虚拟机将没被volatile修饰的64位数据(long和double)的读写操作划分为两次32位操作.即允许虚拟机不保证64位数据的load,read,storewrite的原子性.

4. 原子性,可见性和有序性:

  1. 原子性(Atomicity):

    • 基本数据类型(long和double除外)的读写是具备原子性的.

    • 更大范围的原子性保证:synchronized关键字,Java虚拟机提供monitorentermonitorexit式使用lockunlock操作.

  2. 可见性(Visibility):

    • volatile保证了多线程操作时变量的可见性.

    • sychronized可见性:对一个变量进行unlock之前必须把此变量同步回主内存中.

    • final可见性: 被final修饰的字段在构造器中一旦初始化完成,且构造器没有把this引用传递出去,其他线程中就能看见final字段的值.

  3. 有序性(Ordering):

  • Java提供volatilesynchronized保证线程之间操作的有序性:

    • volatile本身包含禁止指令重排序的语义.

    • synchronized变量在一个时刻只允许一条线程对其进行lock操作,决定同一个锁的两个代码块只能串行进入.

5. 先行发生原则(happens-before):

  • 先行发生原则: 判断数据是否存在竞争,线程是否安全的主要依据.如果操作A先行发生于操作B,在发生B操作之前,操作A产生的影响能被操作B观察到.

  • Java内存模型的先行发生关系:

    1. 程序次序规则:

    在一个线内,按照代码顺序,书写在前面的操作先行发生于写在后面的操作(注意区分先行发生先发生).

    1. 管程锁定规则:

    一个unlock操作先行发生于后面对同一个锁的lock操作.

    1. volatile变量规则:

    对一个volatile变量的写操作先行发生于后面对这个变量的读操作.

    1. 线程启动规则:

    Thread对象的start()方法先行发生于此线程的每个动作.

    1. 线程终止规则:

    线程中的所有操作都先行发生于对此线程的终止检测.

    1. 线程中断规则:

    对线程interrupt()方法的调用先行发生于被中断线程的代码检测中断事件发生.

    1. 对象终结规则:

    一个对象的初始化完成(构造函数执行结束),先行发生于它的finalize()方法的开始.

    1. 传递性:

    操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C.