(1)缓存一致性协议:当CPU中的某个核心想要将执行完指令的结果写回主内存时,必须先向总线申请获取权限。一旦获取了权限,那么这个线程就能和主内存进行数据交换,并且此时其它CPU正在不断的“嗅探”总线,而一旦嗅探到更新数据的这块内存地址发生了改变,其它的CPU就会立即将自己缓存中这块内存地址缓存的数据设置为无效。而当下次执行指令需要用到这块内存地址的缓存数据时,就会因为缓存已经无效从而必须去主内存中加载最新的数据,然后才执行具体的指令。这种方法同一时刻只会锁定主内存中发生了变化的内存地址对应的缓存行。不会把整个总线锁住,其它缓存行还是可以进行数据交换的。那么CPU是如何解决这个问题的呢?其实就是通过一个lock指令来解决。简单归纳一下几个作用:
- 锁总线/锁缓存行
- lock指令会强制让线程在对缓存中某个数据做出修改后,必须先将修改后的结果同步写回主内存,然后其他的线程嗅探到,缓存数据失效,必须又从主内存中读取最新的数据,然后在执行指令
- 类似于内存屏障的效果
(2)总线:CPU缓存和内存交换数据的介质。只要是CPU缓存想要和内存交换数据,必然要通过总线。
大家都知道,CPU是计算机的心脏,最终由它来执行所有的运算和程序。主内存是数据(包括代码行)存放的地方。在计算机系统中,CPU高速缓存是用于减少处理器访问内存(主)所需平均时间的部件,当处理器发出内存访问请求时,会先查看缓存是否有请求数据,有:则不访问主内存直接返回该数据;无(失效):则先把主内存中的相应数据载入缓存,再将其返回处理器。CPU缓存层级分为L1(<=256k,一级缓存)、L2(256k-8m,二级缓存)、L3(4m-50m,三级缓存,在同插槽的所有core共享L3缓存)三个,缓存层级越小越接近于CPU core,容量越小,速度越快。
(3)内存屏障:内存屏障是一个CPU指令。插入一个内存屏障,相当于高速CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。正是通过阻止屏障两边的指令重排序来避免编译器和硬件的不正确优化而提出的一种解决办法。
什么是缓存行(Cache Line)?
(4)缓存行:缓存行是CPU Cache中的最小单位,CPU Cache由若干缓存行组成,一个缓存行的大小通常是64字节(取决于CPU),也就是说L1、L2、L3是以64k 为一块一块的存放在主内存中的,当然不足64k的数据会和其它数据组成64k进行存放(重点,后面会举例说明存在的问题)。
CPU读取数据也是一块一块的读取,因为效率高(程序用了其中一个字节,可能还会用旁边的字节数据,所以按照64k一起读出来,避免频繁读取),下面就是一张图片来展示

问题1:基于以上基础,来看下面两段java代码,分析执行效率


分析两段代码,代码1的性能肯定慢于代码段2,原因分析:【代码段1】:因为一个long是8个字节,两个元素加起来也就16个字节,极有可能存在主内存中是同一个缓存行,加上因为volatile的可见性,线程1改了arr[0],线程2立马就会去主内存获取最新的,线程2改了arr[1],线程1立马就会去主内存获取最新的,这样性能就大打折扣,这就是伪共享。【代码段2】:因为元素0,1,2,3,4,5,6,7共64字节在一个缓存行中,8,9,10,11,12,13,14,15共64字节在另一个缓存行中,所以,线程1改了arr[0],线程2并不会去获取最新的值。同理,线程2改了arr[8],线程1也不会去获取最新的值,这就是缓存行填充/对齐优化,这个也是很多框架中用到的性能优化点,著名的队列框架disruptor就是用的这种优化
CPU的乱序执行:CPU在进行读等待的同时执行其它指令,是CPU乱序的根源,不是乱,而是提供效率,前提是不会影响最终执行结果
问题2:DCL(double check lock)是否需要volatile?
我们知道,在jvm创建一个对象过程:①内存中申请一块区域,对象的值都是默认的,0或者null ②初始化值 ③栈的对象名引用指向创建的对象地址;通过idea的插件,可以知道,创建一个对象CPU的几条指令,如下:


回到这里,我们分析一下为啥DCL需要volatile关键字修饰
比如,现在有1个线程调用了getInstance(),cpu正在创建这个instance对象,但是只执行了【new #2 <org/adx/test/T01_Object>】和【dup】这2条指令,这个时候系统发现可以优化,就出现了指令重排序的情况,也就是先执行了【astore_1】这条指令,那么 本地变量instance就与new Singleton()对象建立了映射关系,但是对象内部的值还没初始化。此时,刚好有另外一个线程2也调用了这个getInstance(),发现本地变量instance不为空,就直接返回了,那么线程2拿到的对象是一个还没初始化完的对象,在程序中用了里面的变量值。就会出现大问题,所以,通过以上问题,就必须加上volatile修饰,禁止对这个对象进行指令重排序,就不会出现这个情况
本文探讨了缓存一致性协议、内存屏障的概念及其在多线程编程中的应用,特别是介绍了如何利用缓存行填充和对齐优化提升性能,以及DCL在Java中的实现细节。

被折叠的 条评论
为什么被折叠?



