美文网首页
伪共享以及对应的解决办法

伪共享以及对应的解决办法

作者: 睦月MTK | 来源:发表于2020-04-21 17:04 被阅读0次

声明:关于概念相关的部分,本文不会着重讲述,请参阅列出的参考文档


一、什么是伪共享
  • 什么是缓存行
    众所周知,CPU有缓存,CPU会从主存中读取数据到缓存中,而读取的单元大小便是一个缓存行大小,如果两个数据在同一个读取单元上,则他们会被读取到CPU的同一个缓存行上。同理,将缓存数据刷新到主存也是同样的道理。总之,缓存行就是这么一个单元化的东西。
  • 缓存行与伪共享的关系
    如果两个变量存在于内存的同一个缓存行上,这时A线程需要对内存上的A变量进行操作,而B线程要对内存上的B变量进行操作,不管谁先操作,谁都要先对该部分内存区域进行锁定(如果不这样,假设A先一步将操作结果刷新到主存,而B后一步刷新到主存,B只修改了变量B,A变量还是原来的,这样当整个缓存行刷回去时就会把A线程所做的操作全部覆盖掉了),锁定了之后,B只能够等待A把缓存内容刷新回去了再进行操作,这就浪费了大量时间。至于为什么把这种现象称为“伪共享”,是因为A和B变量表面上看似乎是独立操作的,应该不存在并发问题,但是仅仅因为他们共同存在于一个缓存行,就造成了和普通并发一样的问题(即需要锁来控制操作顺序)。

二、一个发生了伪共享的代码示例

该段代码将会创建两个线程,两个线程将会同时操作一个对象,只不过t1线程操作对象的v1域,而t2线程操作对象的v2域,两个操作都将执行10亿次。

  • Target
public class Target implements  BaseTarget{
    private volatile long v1;
    private volatile long v2;

    @Override
    public void setV1(long v1) {
        this.v1 = v1;
    }

    @Override
    public void setV2(long v2) {
        this.v2 = v2;
    }
}
  • Main
public class Main {
    //...
    public static void main(String[] args) throws InterruptedException {
        falseSharingTest(false);
    }

    public static void falseSharingTest(boolean optimize){
        int times = 1000000000;
        BaseTarget target = null;
        if(optimize){
            target = new BetterTarget();
        }else{
            target = new Target();
        }
        final BaseTarget finalTarget = target;
        //我的机器是4核的所以可以这么写
        fixedThreadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() - 1);
        fixedThreadPool.submit(() -> {
            for(int i =0; i < times ; i++){
                finalTarget.setV1(i);
            }
        });
        fixedThreadPool.submit(() -> {
            for(int i =0; i < times ; i++){
                finalTarget.setV2(i);
            }
        });
        long start = System.nanoTime();
        threadPoolShutdown();
        System.out.println(Duration.ofNanos(System.nanoTime() - start));
    }
    //...
}
  • 执行结果
PT4.5802031S

三、解决方案及示例
  • 解决方案
    Java8之前可以使用votile的变量进行填充,直到把目标变量挤入下一个缓存行。
    Java8则可以使用@Contended注解,该注解会帮你做好一切事情,对了如果要使其生效的话需要在虚拟机参数中添加下列参数:
-XX:-RestrictContended
  • BetterTarget
public class BetterTarget implements  BaseTarget{
    private volatile long v1;
    @Contended
    private volatile long v2;

    @Override
    public void setV1(long v1) {
        this.v1 = v1;
    }

    @Override
    public void setV2(long v2) {
        this.v2 = v2;
    }
}
  • Main
public class Main {

    public static void main(String[] args) throws InterruptedException {
        falseSharingTest(true);
    }

    public static void falseSharingTest(boolean optimize) throws InterruptedException {
        //略
    }

}
  • 执行结果
PT1.0964604S

一个小小的注解使得性能足足相差了4倍!!!

  • 额外
    如果你发现Contended类无法被导入,你可以试试这个解决办法

参考文档:
[1] 伪共享(false sharing),并发编程无声的性能杀手

相关文章

网友评论

      本文标题:伪共享以及对应的解决办法

      本文链接:https://www.haomeiwen.com/subject/cexuihtx.html