阿里P8的这点Java底层?-Java内存模型、volatile(底层详解)

Java内存模型

JMM

Java Memory Model,JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存工作内存对应的是寄存器和高速缓存

内存交互操作指令

  • 指令说明
  1. lock(锁定)
    主内存中的变量标识为线程独占状态
  2. unlock(解锁)
    将线程独占状态的主内存变量解除锁定
  3. read(读取)
    主内存变量值传输到线程工作内存中
  4. load(载入)
    将read操作传输到工作内存中的值放入工作内存的变量副本中
  5. use(使用)
    使用到变量值的字节码指令执行时,将工作内存中的变量值传递给执行引擎
  6. assign(赋值)
    变量赋值字节码指令执行时,讲执行引擎输出的值赋给工作内存中的变量
  7. store(存储)
    工作内存中变量值传给主内存
  8. write(写入)
    将store操作传输到主内存中的变量值放入到主内存的变量中
  • 内存划分及指令图示
    在这里插入图片描述
  • JMM 8指令使用规则
    1. 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
    2. 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
    3. 不允许一个线程将没有assign的数据从工作内存同步回主内存
    4. 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
    5. 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
    6. 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
    7. 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
    8. 对一个变量进行unlock操作之前,必须把此变量同步回主内存

volatile

开门见山,示例铺路:

  • volatile可见性示例
    在这里插入图片描述

  • volatile无法保证原子性示例
    在这里插入图片描述
    上例预期输出30000,可结果并不能保证

  • 查看字节码文件,发现volatile影响的是属性的标记值,不会影响字节码指令
    在这里插入图片描述
    思考:标记值或者说volatile变量,在jvm将字节码翻译成c++时,都做了什么处理?怎么实现了数据同步?

  • 可见性示例过程图解
    在这里插入图片描述

  • 无法保证原子性示例图解(结合该图理解上图没显示出来的细节)
    在这里插入图片描述

额外的图中lock也会锁住cpu3对主内存的read-load动作(StoreLoad),另外cpu2的store也会被锁住(StroeStore)直到后来缓存被抛弃
图中发现两个CPU可以同时执行read-load操作,从而才有后面故事的发生,若让count不能被同时read不就可以了?(synchronized来一波?)

volatile保证变量线程可见性、有序性、无法保证原子性

  1. 可见性
    我理解的是volatile变量保证了变量变化对各线程可见性
    非volatile变量,在use前不强制进行read-load操作,即执行引擎使用的变量一直是栈中的变量副本,主内存中的变量变化不会同步到栈中。这样当主内存中的变量被其他线程修改了,当前线程不会感知到,上文"volatile可见性示例"中去掉volatile后秀才眼中的pen就会一直是0,书童即使送来了笔,秀才也会一直在那等着;
    volatile变量,第一:变量在use前需先进行read-load操作,也就是执行引擎每次使用变量都是主内存中最新的;第二(原子性、有序性有关):通过java内存屏障操作(写操作完后调用storeload进行内存屏障),我理解storeload就是控制load操作要在store之后执行。
    疑问:这里的写操作是不是就是assign?
  2. 有序性
    volatile变量控制变量写操作先行发生于读操作
    使用storeload内联一段汇编,进行内存屏蔽,防止内存屏蔽位置后的指令被重排序到之前;(原理呢?后面思考中讨论下)
    实际操作中是,jvm内对volatile变量写后立刻执行storeload进行内存屏蔽(汇编的lock前缀作用),从而控制该写操作被cpu写到主内存中之前,cup(多核)不能读到内存中该变量(具体细节下文讨论)
  3. 无法保证原子性
    上文"volatile无法保证原子性示例"中验证结果,为什么呢?
    真正能保证原子性的话,是要控制变量从read-load直到store-write操作之间是原子性的,而volatile只是控制了store到下次read时是顺序的,结合上图理解,多线程下,多线程多cpu时明显能同时进行read-load的。也就是说上例中可能两个线程都读到count=0并执行了count++,但当一个线程写count到内存触发storeload(实际上汇编lock)操作,致其他线程缓存失效,也就是第二个线程的count++结果会被抛弃,这样count最后值明显就会比预期要小。
    一般volatile和synchronized结合使用

balabala~说了一堆,我都不知道我在说啥,哈哈,看不懂的往后看,看完后面的回来再见

思考: volitaile的有序性和可见性底层实现和原理是什么?

  1. 看一下jvm在对volitaile变量进行写的时候做了什么
    在这里插入图片描述
    上图逻辑a.判断volatile b.store操作 c.调用storeload d.storeload方法中内联了一段汇编(实现了内存屏障)
  2. storeload即java中的内存屏障,它是通过内联汇编实现的,如下(我也不懂,先假模假样的看看吧):
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
    如上内联汇编后(摘自:深入理解java虚拟机):
    在这里插入图片描述
    LOCK用于在多处理器中执行指令时对共享内存的独占使用。它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效;另外还提供了有序的指令无法越过这个内存屏障的作用,也就解决了
    上面这些能理解这些吗? 理解好像又不理解?额~ 好吧,下面再补点货吧

扩展1cpu的写操作、lock怎么引起他核无效化Cache的

  1. cpu的写操作有两中方式
    a. 同步写:cpu输出数据直接保存到主内存中;
    b. 异步写:cpu输出的数据线保存在缓存中,待cpu空闲时将缓存同步到主内存中。
    异步写因为主内存的速度跟不上cpu的处理速度,所以通过高速缓存来提升cpu执行效率。虽然这个异步过程很快但是高压下这个在多核心处理同一变量时依然有问题(什么问题?自己想!),lock顺带解决了这个问题
  2. lock的作用
    上面“无法保证原子性示例图解”图中所示,lock通过锁住地址总线从而锁住目标内存区域的read并通过总线嗅探机制使其他cpu抛弃缓存。这就理解了上面提到的lock怎么解决cpu异步写的问题了

扩展2cpu的缓存知识

  • ALU:CPU计算单元,加减乘除都在这里算
  • PC:寄存器,ALU从寄存器读取一次数据为一个周期,需要时间小于1ns
  • L1:1级缓存,当ALU从寄存器拿不到数据的时候,会从L1缓存去拿,耗时约1ns
  • L2:2级缓存,当L1缓存里没有数据的时候,会从L2缓存去拿,耗时约3ns
  • L3:3级缓存,一颗CPU里的双核共用,L2没有,则去L3去拿,耗时约15ns
  • RAM内存:当缓存都没有数据的时候,会从内存读取数据
  • 缓存行:CPU从内存读取数据到缓存行的时候,是一行一行的缓存,每行是64字节(现代处理器)

注意:这里有个缓存行失效问题,会导致性能降低。举例描述就是:[volatile x, volatile y],因缓存行的存在,该数组内两个元素会一起加载到cup缓存中,当cup1对x变量操作并store时会触发lock(汇编),其他线程或者说cup2在操作y时就会因缓存失效而导致y也必须从主内存中重新加载,从而增加性能损耗,这就是缓存行失效。

思考:为什么volatile无法保证原子性?
见上文

疑问:lock(汇编)的范围是什么,StoreStore和StoreLord仅限当前变量?还是当前线程变量?
下文jvm规范能看出范围是控制在当前volatile变量中,具体细节不知道了,有其他补充资料的话再补货吧。。

从JVM规范层面看volatile变量

该小节部分内容摘自:volatile如何保证并发编程中的可见性和有序性?原子性为何不行?

  1. JVM规范中定义的JSR内存屏障定义:
  • LoadLoad屏障:
    对于语句Load1;LoadLoad;Load2;Load1和Load2语句不允许重排序。
  • StoreStore屏障:
    对于语句Store1;StoreStore;Store2;Store1和Store2语句不允许重排序。
  • LoadStore屏障:
    对于语句Load1;StoreStore;Store2;Load1和Store2语句不允许重排序。
  • StoreLoad屏障:
    对于语句Store1;StoreStore;Load2;Store1和Load2语句不允许重排序。
  1. JVM层面volatile的实现要求
  • volatile写操作
    在这里插入图片描述
    操作前面StoreStoreBarrier:保证前面所有的store操作都执行完了才能对当前volatile修饰的变量进行写操作;
    操作后面StoreLoadBarrier:保证后面所有的Load操作必须等volatile修饰的变量写操作完成。
  • volatile读操作
    在这里插入图片描述
    操作后面LoadLoadBarrier:必须等当前volatile修饰变量读操作完成才能读;
    操作后面LoadStoreBarrier:必须等当前的volatile修饰变量读操作完成才能写。

你可能感兴趣的:(java)