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

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

作者: 睦月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