Java 缓存行 和 伪共享

Java 缓存行和伪共享

最近看了一本书,因为以前不太了解底层原理,所以这块比较薄弱,所以通过本文做下记录和总结。

1.缓存行和伪共享的概念

1.1 概念阐述

在计算机系统中,内存是以【缓存行】为单位存储的,一个缓存行存储的字节是2的倍数。不同机器上,缓存行大小也不一样,通常来说为64字节。
伪共享是指:在多个线程同时读写同一个【缓存行】上的不同数据时,尽管这些变量之间没有任何关系,但是在多线程之间仍然需要同步,从而导致性能下降。在多核处理器中,伪共享是影响性能的主要因素之一,通常称之为:“性能杀手”。

1.2 图解

可能上述文字阐述的不是很直白,可以通过这个图理解下:
线程a 在CPU1上读写变量X, 同时线程b上读写变量Y,但是巧合的是变量 X , Y 在同一个缓存行上,那边线程a,b就要互相竞争获取该缓存行的读写权限,才可以进行读写。
假如 线程a在内核1上 获取了缓存行的读写权限,进行了操作,就会导致其他内核中的x变量和y变量同时失效,那么线程b 就必须刷新它的缓存后才能在内核2上获取缓存行的读写权限。 这就导致了这个缓存行在不同的线程之间多次通过 L3缓存进行交换最新复制的数据,极大的影响了CPU性能,如果CPU不在同一个槽上,性能更糟糕。
在这里插入图片描述

2、 JVM内存模型——Java对象结构

在这里插入图片描述

2.1 Java对象头

Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。

所有java 对象都有8字节的对象头,前4个字节用于保存对象的哈希码(前3个字节)和对象锁状态(后一个字节),假如对象处于上锁状态,这4个字节都会被拿到对象外,并用指针进行连接。
剩下的4个字节用来存储对象所属类的引用。
另外,对于数组而言,还有一个保存数组大小的变量,也是4个字节。

2.2 实例对象

对象的实例就是我们在Java对象中看到的属性及其值。

2.3 对齐填充

JVM 要求Java的对象占用的内存大小必须是8bit的整数倍,所以后面有几个字节用于把对象的大小补齐值8bit 倍数。

每个java 对象都会对齐到8字节的倍数,不够会进行填充,为了保证效率,Java 编译器通过字段类型进行了排序:

顺序类型字节数量(字节)
1double8
2long8
3int4
4float4
5Short2
6char2
7boolean1
8byte1
9对象引用4或者8
10子类字段重新排序

3、Demo验证

这里采用书本的样例,可以直接运行,可以调整线程数量 来观摩对性能的影响。


/**
 * @author zhanghuilong
 * @desc Java中缓存行,伪共享的理解
 * @since 2019/06/12
 */
public class FalseSharingDemo {
    // 测试使用线程数
    private final static  int NUM_THREADS= 4;
    // 测试次数
    private final static  int NUM_TEST_TIMES= 10;

    // 无填充 无缓存行对齐的对象类 普通热变量
    static class PlainHotVariable {
        // 1个long 类型变量 占用内存 1*8 = 8 字节,
        public volatile long value = 0L;
    }

    // 有填充 有缓存行对其的对象类
    static class AlignHotVariable extends PlainHotVariable{
        // 用于填充,6个long 类型的变量 ,总占用内存为 6*8 = 48 字节
        // 加上继承父类的一个变量value,那么总共该对象 占用内存为:8字节对象头 + 8字节父类变量+ 6*8字节填充变量 = 64字节,
        // 正好满足一个对象全部在一个缓存行中,消除了伪竞争问题
        public long p1,p2,p3,p4,p5,p6;
    }

    // 竞争者
    static final class CompetitorThread extends Thread {
        //迭代次数
        private final static long ITERATIONS = 500L * 1000L * 1000L;

        private PlainHotVariable plainHotVariable;

        public CompetitorThread(final PlainHotVariable plainHotVariable) {
            this.plainHotVariable = plainHotVariable;
        }

        @Override
        public void run() {
            for (int i=0;i<ITERATIONS;i++){
                plainHotVariable.value = i;
            }
        }
    }



    public static long runOneTest(PlainHotVariable[] plainHotVariables) throws Exception{
        //开启多个线程进行测试
        CompetitorThread[] competitorThreads = new CompetitorThread[plainHotVariables.length];
        for (int i = 0; i < plainHotVariables.length; i++) {
            competitorThreads[i] = new CompetitorThread(plainHotVariables[i]);

        }
        final long start = System.nanoTime();
        for (Thread thread : competitorThreads){
            thread.start();
        }

        for (Thread thread : competitorThreads){
            thread.join();
        }

        return System.nanoTime() - start;
    }


    public static boolean runOneCompare(int threadNum)throws Exception{
        PlainHotVariable[] plainHotVariables = new PlainHotVariable[threadNum];
        for (int i = 0; i < threadNum; i++) {
            plainHotVariables[i] = new PlainHotVariable();
        }
        // 进行无填充 无缓存行对齐测试
        long t1 = runOneTest(plainHotVariables);
        AlignHotVariable[] alignHotVariables = new AlignHotVariable[threadNum];

        for (int i = 0; i < NUM_THREADS; i++) {
            alignHotVariables[i] = new AlignHotVariable();
        }
        // 进行填充 有缓存行对齐的测试
        long t2 = runOneTest(alignHotVariables);

        System.out.println("无填充 无缓存行对齐Plain:"+ t1);
        System.out.println("有填充 有缓存行对齐Plain:"+ t2);

        // 返回结果对比
        return t1 > t2;
    }


    public static void runOneSuit(int threadsNum, int testNum) throws Exception{
        int expectedCount = 0;
        for (int i = 0; i < testNum; i++) {
            if (runOneCompare(threadsNum)){
                expectedCount++;
            }
        }
        //计算有填充 有缓存对其的测试场景下响应时间更短的概率
        System.out.println("Radio (Plain < Align ):" + expectedCount * 100D / testNum +"%");
    }

    public static void main(String[] args) throws Exception{

        runOneSuit(NUM_THREADS, NUM_TEST_TIMES);
    }
}

测试结果:

无填充 无缓存行对齐Plain:17638579323
有填充 有缓存行对齐Plain:7270967980
无填充 无缓存行对齐Plain:20392022924
有填充 有缓存行对齐Plain:7521045611
无填充 无缓存行对齐Plain:12537116135
有填充 有缓存行对齐Plain:7369732614
无填充 无缓存行对齐Plain:12498607757
有填充 有缓存行对齐Plain:7419091587
无填充 无缓存行对齐Plain:12198918237
有填充 有缓存行对齐Plain:7422019467
无填充 无缓存行对齐Plain:11988160497
有填充 有缓存行对齐Plain:7532538573
无填充 无缓存行对齐Plain:12223106752
有填充 有缓存行对齐Plain:7340185594
无填充 无缓存行对齐Plain:16388926182
有填充 有缓存行对齐Plain:7300794683
无填充 无缓存行对齐Plain:12338497888
有填充 有缓存行对齐Plain:7804763605
无填充 无缓存行对齐Plain:12393832355
有填充 有缓存行对齐Plain:7360913752
Radio (Plain < Align ):100.0%

4、伪共享 解决方案

此条信息也是碰巧在这篇文章中看到的:https://blog.csdn.net/hanmindaxiongdi/article/details/81159314

Java8中已经提供了官方的解决方案,Java8中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置-XX:-RestrictContended才会生效。
对应到代码里在AlignHotVariable 类上加上注解,并且去掉填充6个对象,运行时在jvm里开启响应参数即可:

    // 有填充 有缓存行对其的对象类
    @sun.misc.Contended 
    static class AlignHotVariable extends PlainHotVariable{
        // 用于填充,6个long 类型的变量 ,总占用内存为 6*8 = 48 字节
        // 加上继承父类的一个变量value,那么总共该对象 占用内存为:8字节对象头 + 8字节父类变量+ 6*8字节填充变量 = 64字节,
        // 正好满足一个对象全部在一个缓存行中,消除了伪竞争问题
//        public long p1,p2,p3,p4,p5,p6;
    }

以下是测试结果和 有填充项的基本一致:

无填充 无缓存行对齐Plain:19445985843
有填充 有缓存行对齐Plain:6996708098
无填充 无缓存行对齐Plain:12654238078
有填充 有缓存行对齐Plain:8071548517
无填充 无缓存行对齐Plain:19983041578
有填充 有缓存行对齐Plain:7074076269
无填充 无缓存行对齐Plain:17710330823
有填充 有缓存行对齐Plain:7030274857
无填充 无缓存行对齐Plain:20281301886
有填充 有缓存行对齐Plain:7048077452
无填充 无缓存行对齐Plain:19447443573
有填充 有缓存行对齐Plain:7066423588
无填充 无缓存行对齐Plain:20154352370
有填充 有缓存行对齐Plain:7052431719
无填充 无缓存行对齐Plain:18240658823
有填充 有缓存行对齐Plain:6996498595
无填充 无缓存行对齐Plain:19922299237
有填充 有缓存行对齐Plain:7094801775
无填充 无缓存行对齐Plain:17513743086
有填充 有缓存行对齐Plain:7002876176
Radio (Plain < Align ):100.0%
  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值