FalseSharing

走远了吗. 提交于 2019-11-29 04:13:42

FalseSharing

CPU缓存架构

  • CPU 和主内存间有好几级缓存,
    • 直接访问主内存很慢
  • 若对一块数据做相同的运算多次,执行运算时把它加载到离 CPU 很近的地方
  • 如循环计数,不想每次循环都跑到主内存去取这个数据来增长它吧。

在这里插入图片描述

  • L1 很小很快,紧靠着在使用它的 CPU 内核。
  • L2 大些,慢些,只能被一个单独的核使用。
  • L3 在现代多核机器中更普遍,更大,更慢,且被单个插槽上的所有CPU核共享。
  • 主存保存着程序运行的所有数据,更大,更慢,
    • 由全部插槽上的所有 CPU 核共享。
  • CPU运算时,先去L1找,再去L2,然后L3,
    • 最后如果这些缓存中都没有,所需的数据就要去主内存拿。
    • 走得越远,运算耗时就越长。
  • so 若进行很频繁的运算,要确保数据在 L1 缓存。

CPU缓存行

  • 缓存由缓存行组成,常64字节(旧的处理器缓存行32)

    • 它有效地引用主内存中的一块地址。
  • 一个缓存行中可存 8 个 java的long
    在这里插入图片描述

  • 程序运行时,缓存每次更新都从主内存中加载连续64

  • 设想有个long型a,是单独的变量,另个long 型变量 b 挨着它,

    • 当加载a时将免费加载b。
  • 若核的线程在对 a 改,另一个核的线程却对 b 读。

  • 当前者改a 时,会把a和b同时加载到前者核心的缓存行中,更新完a后其它所有包含a的缓存行都将失效

    • 因为其它缓存中的a不是最新值了。
      在这里插入图片描述
  • 这就出现问题,

  • b 和 a 不相干,每次却要因为 a 的更新需要从主内存重新读取,它被缓存未命中给拖慢了。

  • 此即传说中的伪共享。

伪共享

  • 当多线程修改互相独立的变量时,若这些变量共享同一个缓存行,就会无意中影响彼此的性能,
    • 即伪共享。
  • 下面例子说明了伪共享是咋肥事。
public class FalseSharingTest {

    public static void main(String[] args) throws InterruptedException {
        testPointer(new Pointer());
    }

    private static void testPointer(Pointer pointer) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.x++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.y++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(System.currentTimeMillis() - start);
        System.out.println(pointer);
    }
}
class Pointer {
    volatile long x;
    volatile long y;
}
  • 声明Pointer类,含x和y (必须声明为volatile,保证可见性,关于内存屏障后面再讲),一个线程对 x 自增1亿,一个线程对 y 自增1亿。
  • x 和 y 没关系,但更新 x 时会把其它包含 x 的缓存行失效,同时也就失效了 y,运行这段程序输出的时间为3890ms。

避免伪共享

  • 一个缓存行64 字节,
  • long 8 字节,
  • 避免伪共享很简单

在两个 long 类型的变量之间再加 7 个 long 类型

  • 把Pointer改成下面
class Pointer {
    volatile long x;
    long p1, p2, p3, p4, p5, p6, p7;
    volatile long y;
}
  • 运行程序,时间为695

重新创建自己的 long 类型,而不是 java 自带的 long

  • 修改Pointer
class Pointer {
    MyLong x = new MyLong();
    MyLong y = new MyLong();
}

class MyLong {
    volatile long value;
    long p1, p2, p3, p4, p5, p6, p7;
}
  • pointer.x++; 改pointer.x.value++;,pointer.y++; 改为pointer.y.value++;,
  • 间724

使用 @sun.misc.Contended 注解(java8)

  • 修改 MyLong 如下:
@sun.misc.Contended
class MyLong {
    volatile long value;
}
  • 默认使用这个注解是无效的,需要在JVM启动参数加上-XX:-RestrictContended才生效,,
  • 再次运行程序718ms。
  • 以上三种方式中的前两种是通过加字段的形式实现的,加的字段没有地方使用,
    • 可能会被jvm优化掉,建议用第三种。

参考链接

  • https://www.jianshu.com/p/7758bb277985
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!