先看一个小程序
在下面的小程序中,2个线程同时对数组array的第1个,第2个元素进行修改,每个线程修改1千万次。
public class Cacheline_notPadding { public static class T { private volatile long x = 0L;// 占8字节 } private static T[] array = new T[2]; static { array[0] = new T(); array[1] = new T(); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (long i = 0; i < 1000_0000L; i++) { array[0].x = i;// 伪共享问题+缓存一致性协议在修改数据时会消耗额外的时间 } }); Thread thread2 = new Thread(() -> { for (long i = 0; i < 1000_0000L; i++) { array[1].x = i;// 伪共享问题+缓存一致性协议在修改数据时会消耗额外的时间 } }); long startTime = System.nanoTime(); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("总计消耗时间:" + (System.nanoTime() - startTime) / 100_000); } }
执行该小程序,总计消耗时间为:2565。实际上,该小程序存在一个细节问题,是可以进行优化的。这个细节问题就是缓存行伪共享问题。
众所周知,cpu将数据加载到缓存中的最小数据单位是行,缓存中也是以缓存行为单位进行存储的。缓存行的大小一般为32-256个字节,最常见的缓存行大小是64个字节(本文中的示例环境中的缓存行大小为64个字节)。缓存行的最低容量限制带来了一个问题,就是伪共享问题。如下图所示,在本文的小程序中,线程thread1在虽然修改只是array的第一个元素(new T()),但是因为缓存行的最小容量是64字节,而由于第一个元素(new T())中只有一个占8字节的x,所以array所在的缓存行实际上不止包含第一个元素(new T()),也包含第二个元素(new T()),但是实际上在缓存C1的array所在的缓存行在计算时是不需要array的第二个元素的,缓存C2中的array所在的缓存行在计算时也不需要array的第一个元素。这样在缓存一致性协议作用下(这里以MESI协议为例),当线程thread1修改了缓存C1中的array的第一个元素,那么势必会通过总线通知缓存C2作废array的第2个元素,线程thread2修改array中的第二个元素时也是如此,这样势必会带来额外的性能消耗。
那么怎么解决由于缓存行伪共享+缓存一致性协议带来的额外的性能消耗呢?答案就是“缓存行对齐”。如下图所示,如果让缓存C1及C2中的array所在的缓存行只包含一个元素,那么array中的2个元素存在2个不同的缓存行,在计算时就互不影响了。
针对本文的小程序,采用缓存行对齐优化后的代码如下:
在类T中,除了成员属性x(占8个字节),再定义无任何使用意义的7个long类型的成员属性p1...p7(占56个字节),这样就会让一个T对象至少占满8+56=64个字节,这样array每个元素所在的缓存行只能容下一个T对象了,由于array中的两个元素各自独占一个缓存行,那么线程thread1和thread2在计算时就不会互相影响了。
public class Cacheline_Padding { public static class T { private long p1, p2, p3, p4, p5, p6, p7;// 占7*8字节 private long x = 0L;// 占8字节 } private static T[] array = new T[2]; static { array[0]=new T(); array[1]=new T(); } public static void main(String[]args)throws InterruptedException{ Thread thread1=new Thread(()->{ for(long i=0;i< 1000_0000L;i++){ array[0].x=i; } }); Thread thread2=new Thread(()->{ for(long i=0;i< 1000_0000L;i++){ array[1].x=i; } }); long startTime=System.nanoTime(); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000); } }
执行程序,总计消耗时间:99
实际上,本文这样定义无实际使用意义的成员属性来达到缓存行对齐的方式在一些框架源码中是有运用的,如在JDK7的LinkedBlockingQueue源码及Disruptor框架。
缓存行对齐的其他方式
到了JDK8,对于缓存行对齐有了一种更加优雅的解决方式,那就是sun.misc.Contended注解,这个注解直接在类上定义就可以了。
@sun.misc.Contended public static class T { // private long p1, p2, p3, p4, p5, p6, p7;// 占7*8字节 private long x = 0L;// 占8字节 }
注意:如果此注解无效,需要在JVM启动时设置-XX:-RestrictContended。
原文:https://www.cnblogs.com/wql025/p/14671670.html