局部性原理
时间局部性:如果数据正在被访问,那么在近期它很可能还会被再次访问。
比如循环、方法的反复调用等。
空间局部性:如果存储器的位置被引用,那么将来他附近的位置也会被引用。
比如顺序结构、数组。
CPU缓存
执行程序是靠CPU执行主存中代码,但是CPU和主存的速度差异是非常大的,为了降低这种差距,在架构中使用了CPU缓存,现在的计算机架构中普遍使用了缓存技术。常见一级缓存、二级缓存、三级缓存,数据获取访问速度如下:从CPU到 | 大约需要的 CPU 周期 | 大约需要的时间 |
主存 | 约60-80纳秒 | |
QPI 总线传输 (between sockets, not drawn) | 约20ns | |
L3 cache | 约40-45 cycles, | 约15ns |
L2 cache | 约10 cycles, | 约3ns |
L1 cache | 约3-4 cycles, | 约1ns |
寄存器 | 1 cycle |
伪共享问题
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2,L3)后再进行操作,但操作完之后不知道何时会写到内存;如果对声明了volatile 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存。但就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读取到处理器缓存里。 为了说明伪共享问题,下面举一个例子进行说明:两个线程分别对两个变量(刚好在同一个缓存行)分别进行读写的情况分析。 在core1上线程需要更新变量X,同时core2上线程需要更新变量Y。这种情况下,两个变量就在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新对应的变量。如果core1获得了缓存行的所有权,那么缓存子系统将会使core2中对应的缓存失效。相反,如果core2获得了所有权然后执行更新操作,core1就要使自己对应的缓存行失效。这里需要注意:整个操作过程是以缓存行为单位进行处理的,这会来来回回的经过L3缓存,大大影响了性能,每次当前线程对缓存行进行写操作时,内核都要把另一个内核上的缓存块无效掉,并重新读取里面的数据。如果相互竞争的核心位于不同的插槽,就要额外横跨插槽连接,效率可能会更低。缓存行对齐
基于以上问题的分析,在一些情况下,比如会频繁进行操作的数据,可以根据缓存行的特性进行缓存行对齐(即将要操作的数据凑一个缓存行进行操作)下面使用一个示例进行说明:package com.example.demo; public class Cacheline_nopadding { public static class T{ //8字节 private volatile long x = 0L; } private static T[] arr = new T[2]; static { arr[0] = new T(); arr[1] = new T(); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(()->{ for(long i = 0;i < 1000_0000L;i++){ //volatile的缓存一致性协议MESI或者锁总线,会消耗时间 arr[0].x = i; } }); Thread thread2 = new Thread(()->{ for(long i = 0;i< 1000_0000L;i++){ arr[1].x = i; } }); long startTime = System.nanoTime(); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000); } }
总计消耗时间:3381对齐缓存行:
private static class Padding{ //7*8字节 public volatile long p1,p2,p3,p4,p5,p6,p7; } public static class T extends Padding{ //8字节 private volatile long x = 0L; }
优化后:
package com.example.demo; public class Cacheline_padding { private static class Padding{ //7*8字节 public volatile long p1,p2,p3,p4,p5,p6,p7; } public static class T extends Padding{ //8字节 private volatile long x = 0L; } private static T[] arr = new T[2]; static { arr[0] = new T(); arr[1] = new T(); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(()->{ for(long i = 0;i < 1000_0000L;i++){ //volatile的缓存一致性协议MESI或者锁总线,会消耗时间 arr[0].x = i; } }); Thread thread2 = new Thread(()->{ for(long i = 0;i< 1000_0000L;i++){ arr[1].x = i; } }); long startTime = System.nanoTime(); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000); } }
总计消耗时间:1428从上面可以看到,使用缓存对齐,相同操作情况下对齐后的时间比没对齐的时间减少一半。 上面这种缓存行填充的方法在早期是比较流行的一种解决办法,比较有名的Disruptor框架就采用了这种解决办法提高性能,Disruptor是一个线程内通信框架,用于线程里共享数据。与LinkedBlockingQueue类似,提供了一个高速的生产者消费者模型,广泛用于批量IO读写,在硬盘读写相关的程序中应用十分广泛,Apache旗下的HBase、Hive、Storm等框架都有使用Disruptor。
JAVA8对伪共享的解决
进入到JAVA8后,官方已经提供了对伪共享的解决办法,那就是sun.misc.Contended注解,有了这个注解解决伪共享就变得简单多了。@sun.misc.Contended public static class T{ //8字节 private volatile long x = 0L; }默认情况下此注解是无效的,需要在JVM启动时设置-XX:-RestrictContended。 标签:arr,缓存,long,问题,static,new,共享,public From: https://www.cnblogs.com/zhengbiyu/p/17273810.html