Java中的伪共享问题

我们知道为解决CPU与内存速度不一致问题,会在它俩中间添加一级或多级高速缓冲存储器(Cache),在Cache内部是按行存储的,每一行成为一个Cache行。Cache行是Cache与主内存进行数据交换的单位,大小一般为2的幂次数字节。

当CPU访问某个变量的时候,首先会看CPU Cache内是否有该变量,如果有则直接取走,否则去主内存里获取该变量,并把该变量所在内存区域的一个Cache行大小的内存复制到Cache中。因为一下子是把一个Cache行大小的内存块放进去,所以可能会导致把多个变量存放到一个Cache行中当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。可以看一下一个例子,如图所示:
在这里插入图片描述
该图中,主内存的一个Cache行块大小的内存被分别放到了两个CPU各自的缓存中,如果此时CPU0中的Thread0在修改其中的红色变量,则需要改变此变量所在的Cache Line,那么根据缓存一致性协议,CPU1中的Cache Line就会失效。若Thread1要修改蓝色变量,则需要重新从主内存中读取这块内存。这也说明了多个线程不可能同时去修改自己所使用的CPU中相同缓存行里面的变量,且可能会导致频繁的去访问主内存。

1.伪共享出现的原因

通过上面的讲述其实也能明白为什么会出现这种问题,因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中不同的变量。由于缓存与内存交换数据的单位是缓存行,当访问一个变量时,会把这个变量所在内存中大小为缓存行的内存放入缓存行,所以多个变量会放在一个缓存行中。举一个例子:

long a;
long b;
long c;
long d;

声明了四个long变量,假设缓存行大小为32字节,当CPU访问变量a时,发现它没有在缓存中,就会去主内存中把变量a以及内存地址附近的b、c、d放入缓存行。所以地址连续的多个变量才有可能会被放到一个缓存行中。当创建数组时,数组里的多个元素就会被放入同一个缓存行中。

当然如果只是单线程的话,那么多个变量放入一个一个缓存行中队代码执行是有利的,代码会执行更快,看一下下面的代码:

public class TestForContent {

    static final int LINE_NUM = 1024;
    static final int COLM_NUM = 1024;

    public static void cache() {
        long[][] array = new long[LINE_NUM][COLM_NUM];
        long startTime = System.currentTimeMillis();
        //数组是按行存储的,所以接近顺序取数据
        for (int i = 0; i < LINE_NUM; ++i) {
            for (int j = 0; j < COLM_NUM; ++j) {
                array[i][j] = i*2 + j;	
            }
        }

        long endTime = System.currentTimeMillis();
        long cacheTime = endTime - startTime;
        System.out.println("cache time:" + cacheTime);
    }

    public static void nocache() {
        long[][] array = new long[LINE_NUM][COLM_NUM];
        long startTime = System.currentTimeMillis();
        //数组是按行存储,所以不是顺序取数据
        for (int i = 0; i < COLM_NUM; ++i) {
            for (int j = 0; j < LINE_NUM; ++j) {
                array[i][j] = i*2 + j;
            }
        }

        long endTime = System.currentTimeMillis();
        System.out.println("no cache time:" + (endTime - startTime));
    }

    public static void main(String[] args) {
        cache();
        nocache();
    }

}

cache()函数基本接近于顺序取数据,而nocache()则是跳跃式访问数组元素,可能执行过程中会有多次淘汰cache的情况出现,所以慢一些。所以在单个线程下修改一个缓存行中的多个变量,会充分利用程序运行的局部性原则,加速程序运行。而在多线程下并发修改一个缓存行中的多个变量时就会竞争缓存行,从而降低程序运行性能。

2. 如何避免伪共享

JDK8之前一般是通过字节填充的方式避免该问题,==也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中,如下:

public final static class FilledLong {
	public volatile long value = 0L;
	public long p1, p2, p3, p4, p5, p6;
}

假如缓存行是64字节,那在FilledLong类里填充了6个long类型的变量,每个long变量占8字节,加上value一共56字节,然后FilledLong是一个类对象,类对象的字节码的对象头占用8字节,所以一个FilledLong对象实际会占用64字节的内存,正好可以放入一个缓存行。

当然JDK6中提供了更方便的工具:sun.misc.Contended注解用来解决伪共享问题。如下代码:

@sun.misc.Contended
public final static class FilledLong {
	public volatile long value = 0L;
}

这里使用注解修饰的类,当然也可以修饰变量。但要注意,默认情况下@Contended注解只用于Java核心类,比如rt包下的类。如果要使用这个注解,则需要添加JVM参数:-XX:-RestrictContended。填充的宽度默认为128,要自定义宽度可以设置=XX:ContendedPaddingWidth参数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值