JVM:什么是伪共享问题,如何解决伪共享问题(2)

1. 为什么会有伪共享问题

为什么会有伪共享问题就要从CPU多级缓存说起,计算机中每个CPU Core都有自己的多级缓存,CPU操作数据的时候先从自己的Cache中查找数据,如果没有找到,再从内存中读取,然后将读取的数据放到自己的Cache中。

Cache LineCPU从内存读取数据到自己的Cache的单位,一般Linux系统中Cache Line的大小是64字节。也就意味着CPU一次载入的数据大小是64字节。

CPU在取数据到自己的Cache中,除了取目标数据还会取该数据相邻的数据到Cache中,主要是依据时间局部性和空间局部性。

多级缓存又会导致缓存一致性问题,为了解决这个问题,又提出了缓存一致性协议MESIMESI是为了解决缓存一致性的问题,但是也引入了另外一个问题就是伪共享问题。

关于MESI可参考:JVM:Java内存模型(1)

2. 伪共享产生的流程

假设现在有两个CPU Core,分别是Core1Core2,这两个Core在运算过程中,Core1需要变量xCore2需要变量yxy都是long类型的变量,在内存中的地址是连续的。

2.1 缓存数据

1.由于CPU每次读取数据都会读取Cache Line大小,也就是64个字节根据空间局部性原理。

2.Core1在缓存x的同时也会缓存y,并且xy在同一个Cache Line当中。

3.同理Core2在缓存y的时候,也要缓存xxy也在同一个Cache Line当中。

4.这个时候两个Cache Line的为Shared状态。

在这里插入图片描述

2.2 Core1修改数据

1.现在Core1要修改x变量的值,发现x所在的Cache LineShared状态

2.Core1需要先广播,然后Core2收到这个广播之后,把自己的Cache Line设置为Invalid状态。

3.然后Core1修改x的值,然后把Cache Line设置为Modified状态。

在这里插入图片描述

2.3 Core2修改数据

1.现在Core2要修改y的值,发现Cache LineInvalid的状态。

2.然后Core1缓存了相同数据的,并且是Modified,需要先把Core1里面修改了的值同步到内存。

3然后Core2再从内存读取修改后的值读取到自己的缓存中。

4.Core2修改y的值,修改之后把Cache Line标记为Modified的。

5.Core1Cache Line标记为Invalid

在这里插入图片描述

上述流程,虽然x只有Core1进行读写操作,y只有Core2,进行读写操作,按理说直接在缓存中操作即可,但是现在需要不停的从缓存和内存交换数据,如果这是一个循环的话,会很浪费性能。核心原因就在于Cache Line中同时缓存了xy,而xy被不同的CPU Core使用,导致了缓存失效,这就是伪共享问题。

3. 伪共享的代码示例

  1. 现在有一个对象,有三个变量,三个线程他同时对这个三个变量进行修改。
public class ObjDemo {
    volatile long a;
    volatile long b;
    volatile long c;
}
  1. 对这三个变量进行修改,注意join方法是阻塞主线程,防止主线程提前结束了,同时保证t1t2,和t3都能完成。
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        ObjDemo objDemo = new ObjDemo();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                objDemo.a++;
            }
        });

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

        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                objDemo.c++;
            }
        });

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

        System.out.println("总耗时:" + (System.currentTimeMillis() - start));
    }
}


上述代码总共耗时大概2000ms以上,三个线程,在修改对应long类型的变量的时候,需要把变量缓存到自己的Cache Line中,由于Cache Line一次缓存64个字节,而一个long类型的变量是8字节,由于空间局部性,CPU缓存的时候会把相邻的数据拿到自己的缓存中,所以虽然线程t1不使用bc也会把这两个变量放到自己的缓存中,其它两个一样。导致在修改的时候,会通知另外两个线程,并且修改了之后需要同步到内存当中,虽然这个变量没有其它核使用。

4. 解决方法1

第一种办法就是把这个三个变量分开,让它们不会缓存在同一个Cache Line中,最简单的做法就是把变量后面填满,刚好是一个Cache Line,这样另外的变量只能在下一个Cache Line当中了。

package com.lee.study.basic;
public class ObjDemo {
    volatile long a;
    long p1, p2, p3, p4, p5, p6, p7;
  
    volatile long b;
    long q1, q2, q3, q4, q5, q6, q7;

    volatile long c;
}

5. 解决方法2

Java8中提供了一个注解@Contented,使用了此注解的类或者变量会在前后加128字节,同时需要添加虚拟机参数-XX:-RestrictContended才会生效

import sun.misc.Contended;
public class ObjDemo {
    @Contended
    volatile long a;
    
    @Contended
    volatile long b;

    volatile long c;
}

使用上述两种方法可以,把代码运行时间降低到几百毫秒

参考文档

什么是伪共享?又该怎么避免伪共享的问题

彻底搞清楚什么是伪共享

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值