伪共享

java并发编程之美 学习笔记

什么是伪共享

Cache
为了解决计算机系统中主内存和CPU之间的运行速度差问题,会在CPU和主内存之间添加一级或多级高速缓冲存储器(Cache). 这个缓存一般是集成到CPU内部的,所以也叫CPU cache.
在这里插入图片描述

缓存行(Cache行)
在cache内是按行存储的,其中每一行称之为一个Cache行.Cache行是Cache与内存进行数据交换的单位,Cache行的大小一般为2的幂次方字节。
在这里插入图片描述

当CPU访问某个变量时,首先会去CPU cache内是否有该变量

  • 如果有,则字节使用
  • 如果没有,则去主内存中获取该变量,然后把该变量所在内存区域的一个Cache行大小的内存复制到cache汇总。(该内存块中可能包含多个变量)

伪共享
当多线程修改一个cache行的多个变量时,如下图所示:
在这里插入图片描述
当线程1对x进行更新时,首先修改cpu1的cache1中变量x所在的缓存行,在缓存一致性协议下,cpu2中cache1中变量x所在的缓存行就会失效。
那么在线程2对变量y进行修改时,就只能从cache2中去寻找y变量。(如果cpu只有一级缓存,则cpu会频繁的访问主内存)。

当多个线程同时修改一个缓存行里的多个变量时,只允许一个线程操作缓存行,这个就是伪共享.

为何会出现伪共享
伪共享的产生时因为:多个变量被放入了一个缓存行,并且有多个线程更新该缓存行中的不同变量。
为什么多个变量会放入一个缓存行呢?
因为缓存行是cpu和内存交换的数据单位,如果cpu中需要访问变量x,而缓存中没有x,则需要到主内存中寻找x,并将x所在内存地址附近的一个缓存行的数据放入缓存。

那么为什么不把cpu和内存的交换单位改变为单个变量的大小呢,即一个缓存行只有一个变量呢?
先看下面一个例子:

public class Test4Content {
	static final int LINE_NUM = 1024;
	static final int COLUMN_NUM = 1024;

	/**
	 * 数组中内存是连续的,当访问数组中的第一个元素时,会把后续的若干个元素放入缓存行,
	 * 这样顺序访问元素时会在缓存中直接命中,而不用去主内存中读取了。
	 */
	static void cache(){
		long[][] arr = new long[LINE_NUM][COLUMN_NUM];
		long startTime = System.nanoTime();
		for (int i = 0; i < LINE_NUM; i++) {
			for (int j = 0; j < COLUMN_NUM; j++) {
				arr[i][j] = i*2 + j;
			}
		}
		long endTime = System.nanoTime();
		System.out.println("cache time: " +(endTime - startTime) +"ns");
	}

	/**
	 * 跳跃式读取,不是顺序的,而cpu缓存是有容量的,当无法将整个数组放到缓存时,就需要从主内存中读取数据了。
	 */
	static void noCache(){
		long[][] arr = new long[LINE_NUM][COLUMN_NUM];
		long startTime = System.nanoTime();

		for (int i = 0; i < LINE_NUM; i++) {
			for (int j = 0; j < COLUMN_NUM; j++) {
				//跳跃式读取...
				arr[j][i] = i*2 + j;
			}
		}
		long endTime = System.nanoTime();
		System.out.println("no cache time:  " +(endTime - startTime) +"ns");
	}
	public static void main(String[] args) {
		IntStream.rangeClosed(1,10).forEach(index ->{
			cache();
			noCache();
			System.out.println("- -- --- ---- ----- ------ -------");
		});
	}
}

部分运行结果截图:
在这里插入图片描述
上述例中cache()方法比nocache()方法速度上快了不少。
这是因为数组内元素在内存地址上是一段连续的空间,当访问数组第一个元素时,会把后续的若干元素一块放入缓存行,这样顺序访问数组元素时会在缓存中直接命中。

  • cache() 方法-顺序读取: 当元素在缓存中不存在,会从主内存中读取若干个元素到缓存,而后续的读取可以直接命中缓存。
  • nocache()方法-跳跃读取:因为它不是顺序读取,每读取一个元素后就需要再次从主内存中读取元素,但是cpu缓存容量是有限的,当缓存满了之后会采取淘汰算法淘汰缓存行,导致缓存行中的其他元素没有读取到时就已经被淘汰,当使用到该元素时又需要重新从内存中读取。

上面的例子解释了为什么一个缓存行为什么不只存储一个变量:

  • 首先cpu缓存容量是有限的
  • 在单线程程序中,修改一个缓存行的多个变量(如上例中的数组),会利用数组在内存的连续性,加速程序的运行.

如何解决伪共享

在jdk8之前,一般是通过字节填充的方式来避免伪共享问题。
字节填充

class FilledLong{
 	//long 占用8字节
	public volatile long value = 0;
	
	//填充了p1-p6 = 6*8 = 48 字节
	private long p1,p2,p3,p4,p5,p6;
}

填充了6个字段占用48字节,FilledLong对象头8字节,value占用8字节, 正好凑够64个字节,可以放入一个缓存行。

@sun.misc.Contended注解
jdk8 提供了该注解,用来解决伪共享问题:

//注解在类上
@sun.misc.Contended
static class FilledLong{
	public volatile long value = 0;
}

//注解在变量上
public class Thread implements Runnable {
    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;

    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

    @sun.misc.Contended("tlr")
    int threadLocalRandomSecondarySeed;
}

在默认情况下,@Contended注解只用于Java核心类,比如rt.jar下的类。
如果用户类路径下的类需要使用这个注解,则需要添加JNM参数:-XX:-RestrictContended
填充的宽度默认为128,要自定义宽度则可以设置-XX:ContendPaddingWidth参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值