CPU高速缓存与JMM

CPU高速缓存

为了解决CPU越来越快的运行速度与相对较慢的主存访问速度的矛盾。CPU中的寄存器数量有限,在执行内存寻址指令时,经常需要从内存中读取指令所需的数据或是将寄存器中的数据写回内存。引入高速缓存后,CPU在需要访问主存中某一地址空间时,高速缓存会拦截所有对于内存的访问,并判断所需数据是否已经存在于高速缓存中。如果缓存命中,则直接将高速缓存中的数据交给CPU;如果缓存未命中,则进行常规的主存访问,获取数据交给CPU的同时也将数据存入高速缓存。

缓存一致性

在多核CPU的架构下,通常每一个核心都拥有着自己独有的高速缓存,每个核心能并发的读写自己的高速缓存。**高速缓存可以有多个,但其对应的内存数据逻辑上却只有一份,多核并发的修改其高速缓存中同一内存的映射数据就会出现高速缓存中的数据不一致的问题。**如果不对多核CPU下的高速缓存并发访问施加一定的约束,那么并发程序中对共享内存数据的存取就会出现问题,并发程序的正确性将无法得到有效保障

内存屏障

内存屏障有两个作用:

  1. 阻止屏障两侧的指令重排序(禁止CPU指令重排);
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

针对关键的任务间共享变量的读写需要使用内存屏障保证其在多核间高速缓存的一致性。在对共享变量的写入指令后,加入写屏障,令新的数据立即对其它核心可见;在对共享变量的读取指令前,加入读屏障,令其能获取最新的共享变量值。

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。

读屏障:在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载新数据;

写屏障:在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

指令重排

CPU指令重排

 Cpu为了提高效率会对指令进行重排序,以适合cpu的顺序运行。但是指令重排会遵守As-if-serial的规则,就是所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。所以这种情况在单线程中不会出现什么问题。而对于多线程,这个规则就失效了,所以可能会导致结果出现问题。

​ 解决办法就是内存屏障,也叫内存栅栏。是一种屏障指令,cpu指令。Java中的实现方式就是使用volatile关键字,既可以解决可见性,又可以禁止指令重排。

JIT编译指令重排

当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,即时编译器(Just In Time Compiler )会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。

volatile

内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样,java通过volatile这些差异,统一由jvm来生成内存屏障的指令

可见性问题:让一个线程堆共享变量的修改,能够及时的被其他线程看到。

Java内存模型规定:对volatile变量v的写入,与所有其他线程后续堆v的读同步

所以volatile关键字具有如下功能

  1. 禁止缓存;volatile变量的访问控制符会加个ACC_VOLATILE,写入和读取都不能够被缓存,即写入从缓存同步到主内存中,读操作从主内存中读取,使高速缓存失效。
  2. 对volatile变量相关的指令不做重排序

JMM

jit编译示例

public class DemoVisibility {
    int i=0;
    boolean isRunning=true;
    public static void main(String[] args)throws  InterruptedException {
        DemoVisibility demo=new DemoVisibility();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("here i am...");
                while (demo.isRunning){
                    demo.i++;
                }
                System.out.println(demo.i);//当jvm是client模式时可以打印出,为server模式无法打印出
            }
        }).start();
        Thread.sleep(3000L);
        demo.isRunning=false;
        System.out.println("shutdown....");
    }
}

代码解析

上面的代码,运行情况可以用下图表示:子线程当isRunning=true 作为while循环条件,主线程三秒后将isRunning赋值为false,

根据高速缓存协议的存在,CPU间会同步isRunning=false值到高速缓存的,可能短时间内会存在没有及时通报不的情况,但是还是会同步的。但是示例一直没有打印 System.out.println(demo.i),造成这个的原因是jit编译,通过volatile关键字可以禁止jit的缓存和指令重重排
在这里插入图片描述

一直没有打印demo.i图解

如下图所示,jvm一行行执行(解释执行)通过编译器执行前编译后的代码,当方法被多次调用或者方法中的循环体多次调用,解释执行上升为编译执行(jit,会进行指令重排,性能优化,优化后的代码成了while(true),如下所示),优化后的代码放入到方法区中,供jvm调用执行

在这里插入图片描述

在这里插入图片描述

解决方案

通过添加volatile关键字实现禁止缓存和禁止指令重排。

JIT编译器(Just In TIme Compiler)

解释执行:即咱们说的脚本,在执行时,由语言的解释器将其中一条条翻译成机器可识别的指令

在这里插入图片描述

编译执行:将我们编写测程序,直接编译成机器可以识别的指令码。

在这里插入图片描述


执行方式不同,二者的优势各有不同
静态语言(编译方式):编译器一次性生成目标代码,优化更充分,程序运行时速度更快。
1)对于相同的源代码,编译所产生的的目标代码执行速度更快。
2)目标代码不需要编译器就可以运行,在同类操作系统上使用灵活。
脚本语言(解释方式):执行程序时需要源代码,维护更加灵活。
1)解释执行需要保留源代码,程序纠错和维护十分方便。
2)只要存在解释器,源代码可以在任何操作系统上运行,可移植性好。

java的编译执行和解释执行

Java在编译时期,通过将源代码编译成.class ,配合JVM这种跨平台的抽象,屏蔽了底层计算机操作系统和硬件的区别,实现了“一次编译,到处运行” 。 而在运行时期,目前主流的JVM 都是混合模式(-Xmixed),即解释运行 和编译运行配合使用。解释器的优势在于不用等待,编译器则在实际运行当中效率更高。在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,从而提高运行效率.

jit触发的条件(java转为解释执行)

(1)被多次调用的方法

(2)被多次执行的循环体

对于第一种,编译器会将整个方法作为编译对象,这也是标准的JIT 编译方式。对于第二种是由循环体出发的,但是编译器依然会以整个方法作为编译对象,因为发生在方法执行过程中,称为栈上替换。

JMM 包括

Share Variables (共享变量)定义

可以在线程之间共享的内存称为共享内存或堆内存

所有实例字段、静态字段、和数组元素都存储在堆内存中,这些字段和数组都是共享变量

冲突:如果至少一个访问是写操作,那么对同一个变量的两次访问是冲突的。

这些能被多线程访问的共享变量是内存模型规范的对象。

线程间操作的定义

  1. 线程间操作:一个程序执行的操作被其他线程感知或被其他线程直接影响

  2. Java内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行。

    线程间操作有

    1. read操作(一般读,非volatile读)
  3. write操作(一般写,非volatile写)
    3. volatile read

  4. volatile write
    5. Lock(锁monitor)、Unlock

  5. 线程的第一个操作和最后一个操作

外部操作,多线程操作同一个数据库

对于同步的规则定义
  1. volatile变量v的写入,与所有其他线程后续对v的读同步
  2. 对于监视器m的解锁*与所有后续操作对m的加锁**同步(线程t1和t2争抢m锁,t1获取成功对共享变量的操作,t1释放后,t2抢到锁对t1对共享变量的操作可见)

对于每个属性写入默认值(0,false,null)与每个线程对其进行的操作同步。(表示不同线程创建的同一类的对象中的属性初始值一样)

Happens-before

heppens-before关系用于描述两个有冲突的动作之间的顺序,如果一个action happends before另一个action ,则第一个操作被第二个操作可见,JVM需要实现如下happens-before规则:

  1. 某个线程的每个动作都happens-before该线程中该动作后面的动作

  2. 某个管程上unlock动作heppens-before同一管程上后续的lock操作。(同一个锁的unlock操作happens-before此锁的lock操作,多线程情况下,一个线程进行了解锁的操作,对于晚于该操作的加锁能够及时感应到锁的状态变化,解锁的操作对后面加锁的线程可见)

  3. 对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作。(对valatile变量的写操作的结果对于发生于其后的线程的读操作可见)

  4. 在某个线程对象上调用start()方法happens-before被启动线程的任意动作。

  5. 如果在线程t1中成功执行了t2.join,则t2中所有操作对t1可见。(线程A调用B的join方法),当B线程执行完成后,A线程调用Bjoin后的操作,B线程修改共享变量对A可见。

  6. 如果某个动作a happen-before动作b,且 b heppen-before c,则有a happens-before c.

  7. 如果在线程t1中成功执行了t2.join,则t2中所有操作对t1可见。(线程A调用B的join方法),当B线程执行完成后,A线程调用Bjoin后的操作,B线程修改共享变量对A可见。

  8. 如果某个动作a happen-before动作b,且 b heppen-before c,则有a happens-before c.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值