伪共享示例

本文深入探讨了伪共享现象,解释了当多个线程试图同时访问不同变量但位于同一缓存行时,如何导致性能下降。通过三种访问数组的方法对比,展示了如何使用填充数据和JDK 1.8的@Contended注解来避免伪共享,显著提升访问速度。

在这里插入图片描述
一图了解伪共享:
线程1在cpu1工作想改变数值x,线程2在cpu2工作想改变数值y。这时,系统调度的时候就不会让线程1和线程2同时运行,因为一行是cpu缓存修改的最小单位。这种情况就是伪共享。

下面示例分别用三种不同的方式进行访问数组并比较快慢:
第一种单纯地访问数据。
第二种在访问的值左右填满无用数据,使得它独占一行。
第三种使用jdk1.8的@sun.misc.Contended注解。

public class FalseSharing  implements Runnable {
    public final static int NUM_THREADS =
            Runtime.getRuntime().availableProcessors();
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private final int arrayIndex;

    public FalseSharing(final int arrayIndex){
        this.arrayIndex = arrayIndex;
    }

    //用于比较访问数据的时间
    public void run(){
        long i = ITERATIONS + 1;
        while (0 != --i){
            longs[arrayIndex].value = i;
        }
    }
    
    public final static class VolatileLong {
        public volatile long value = 0L;
    }

    // long padding避免false sharing
    // 按理说jdk7以后long padding应该被优化掉了,但是从测试结果看padding仍然起作用
    public final static class VolatileLongPadding {
        public volatile long p1, p2, p3, p4, p5, p6, p7;
        public volatile long value = 0L;
        public volatile long q1, q2, q3, q4, q5, q6, q7;
    }

     //jdk8新特性,Contended注解避免false sharing
    @sun.misc.Contended
    public final static class VolatileLongAnno {
        public volatile long value = 0L;
    }

    //数组里存放的是类
    private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
//    private static VolatileLongPadding[] longs = new VolatileLongPadding[NUM_THREADS];
//    private static VolatileLongAnno[] longs = new VolatileLongAnno[NUM_THREADS];
    static{
        /*将数组初始化*/
        for (int i = 0; i < longs.length; i++){
            longs[i] = new VolatileLong();
//            longs[i] = new VolatileLongPadding();
//            longs[i] = new VolatileLongAnno();
        }
    }



    public static void main(final String[] args) throws Exception{
        final long start = System.nanoTime();
        runTest();
        System.out.println("duration = " + (System.nanoTime() - start));
    }

    private static void runTest() throws InterruptedException{

        Thread[] threads = new Thread[NUM_THREADS];
        for (int i = 0; i < threads.length; i++){
            threads[i] = new Thread(new FalseSharing(i));
        }

        for (Thread t : threads){
            t.start();
        }

        //等待所有线程执行完成,避免主线程在其它线程前结束
        for (Thread t : threads){
            t.join();
        }
    }
}

结果:
第一种:
duration = 26205176237
第二种:
duration = 7477267155
第三种:
duration = 7589874226

可以看到第二种和第三种在访问速度上有数量级上的提升。

点击这里,查看更多关于伪共享的知识。

### 原理 Cache 是以 Cache Line 为单位去内存中取数据并且缓存数据的,一般来说 Cache Line 的大小为 64 字节。当访问 long 类型数组中某个成员时,CPU 会将临近的数组成员都加载到同一个 Cache Line 中,这样可以加速访问。当多个线程去同时读写共享变量时,由于缓存一致性协议,只要 Cache Line 中任一数据失效,整个 Cache Line 就会被置为失效。这就会导致本来相互不影响的数据,由于被分配在同一个 Cache Line 中,双方在写数据时,导致对方的 Cache Line 不断失效,无法利用 Cache Line 缓存特性,这种现象就被称为“共享” [^2][^3]。 ### 影响 共享会导致 Cache Line 被频繁的导入导出,造成很大的性能问题。因为当一个处理器修改了某个 Cache Line 中的数据时,其他处理器中对应的 Cache Line 会失效,需要重新从内存中加载数据,这增加了数据访问的延迟,降低了程序的性能 [^3]。 ### 解决方法 一种解决方法是进行 Cache Line 对齐,即通过合理定义数据结构,将不同线程访问的数据放置在不同的 Cache Line 中。例如,在结构体中添加填充变量。如在 `RingBufferFelds` 里面定义的变量用 `final` 修饰,意味着第一次加载之后不会再修改, 又由于「前后」各填充了 7 个不会被读写的 long 类型变量,所以无论怎么加载 Cache Line,这整个 Cache Line 里都没有会发生更新操作的数据,只要数据被频繁地读取访问,就自然没有数据被换出 Cache 的可能,也因此不会产生共享的问题 [^5]。以下是一个简单的 C 语言示例代码: ```c #include <stdio.h> typedef struct { long x; long padding[7]; // 填充 7 个 long 变量以实现 Cacheline 对齐 long y; } CacheLineAlignedData; int main() { CacheLineAlignedData data; data.x = 10; data.y = 20; printf("x: %ld, y: %ld\n", data.x, data.y); return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值