JAVA中的伪共享与缓存行

news2024/11/17 4:49:22

一.伪共享与缓存行

1.CPU缓存架构

CPU 是计算机的心脏,所有运算和程序最终都要由它来执行。

主内存(RAM)是数据存放的地方,CPU 和主内存之间有好几级缓存,因为即使直接访问主内存也是非常慢的。

CPU的速度要远远大于内存的速度,为了解决这个问题,CPU引入了三级缓存:L1,L2和L3三个级别,L1最靠近CPU,L2次之,L3离CPU最远,L3之后才是主存。速度是L1>L2>L3>主存。越靠近CPU的容量越小。CPU获取数据会依次从三级缓存中查找

当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。一般来说,每级缓存的命中率大概都在80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取,由此可见一级缓存是整个CPU缓存架构中最为重要的部分。

 2.什么是伪共享

计算机系统中为了解决主内存与CPU运行速度的差距,在CPU与主内存之间添加了一级或者多级高速缓冲存储器(Cache),这个Cache一般是集成到CPU内部的,所以也叫 CPU Cache,如下图是两级cache结构

Cache内部是按行存储的,其中每一行称为一个缓存行,缓存行是Cache与主内存进行数据交换的单位,缓存行的大小一般为2的幂次数字节。 

当CPU访问某一个变量时候,首先会去看CPU Cache内是否有该变量,如果有则直接从中获取,否者就去主内存里面获取该变量,然后把该变量所在内存区域的一个Cache行大小的内存拷贝到Cache(cache行是Cache与主内存进行数据交换的单位)。由于存放到Cache行的的是内存块而不是单个变量,所以可能会把多个变量存放到了一个cache行。当多个线程同时修改一个缓存行里面的多个变量时候,由于同时只能有一个线程操作缓存行,所以相比每个变量放到一个缓存行性能会有所下降,这就是伪共享。

 

 如上图变量x,y同时被放到了CPU的一级和二级缓存,当线程1使用CPU1对变量x进行更新时候,首先会修改cpu1的一级缓存变量x所在缓存行,这时候缓存一致性协议会导致cpu2中变量x对应的缓存行失效,那么线程2写入变量x的时候就只能去二级缓存去查找,这就破坏了一级缓存,而一级缓存比二级缓存更快。更坏的情况下如果cpu只有一级缓存,那么会导致频繁的直接访问主内存。

3.为何会出现伪共享

​ 伪共享的产生是因为多个变量被放入了一个缓存行,并且多个线程同时去写入缓存行中不同变量。那么为何多个变量会被放入一个缓存行那。其实是因为Cache与内存交换数据的单位就是Cache,当CPU要访问的变量没有在Cache命中时候,根据程序运行的局部性原理会把该变量在内存中大小为Cache行的内存放如缓存行。

4.Java中的伪共享

 解决伪共享最直接的方法就是填充(padding),例如下面的VolatileLong,一个long占8个字节,Java的对象头占用8个字节(32位系统)或者12字节(64位系统,默认开启对象头压缩,不开启占16字节)。一个缓存行64字节,那么我们可以填充6个long(6 * 8 = 48 个字节)。

​ 现在,我们学习JVM对象的内存模型。所有的Java对象都有8字节的对象头,前四个字节用来保存对象的哈希码和锁的状态,前3个字节用来存储哈希码,最后一个字节用来存储锁状态,一旦对象上锁,这4个字节都会被拿出对象外,并用指针进行链接。剩下4个字节用来存储对象所属类的引用。对于数组来讲,还有一个保存数组大小的变量,为4字节。每一个对象的大小都会对齐到8字节的倍数,不够8字节部分需要填充。为了保证效率,Java编译器在编译Java对象的时候,通过字段类型对Java对象的字段进行排序,如下表所示。

 ​ 因此,我们可以在任何字段之间通过填充长整型的变量把热点变量隔离在不同的缓存行中,通过减少伪同步,在多核心CPU中能够极大的提高效率。

最简单的方式

/**
 * 缓存行填充父类
 */
public class DataPadding {
    //填充 6个long类型字段 8*4 = 48 个字节
    private long p1, p2, p3, p4, p5, p6;
    //需要操作的数据
    private long data;
}

因为JDK1.7以后就自动优化代码会删除无用的代码,在JDK1.7以后的版本这些不生效了

继承的方式

/**
 * 缓存行填充父类
 */
public class DataPadding {
    //填充 6个long类型字段 8*4 = 48 个字节
    private long p1, p2, p3, p4, p5, p6;
}

继承缓存填充类

/**
 * 继承DataPadding
 */
public class VolatileData extends DataPadding {
    // 占用 8个字节 +48 + 对象头 = 64字节
    private long data = 0;

    public VolatileData() {
    }

    public VolatileData(long defValue) {
        this.data = defValue;
    }

    public long accumulationAdd() {
          //因为单线程操作不需要加锁
         data++;
        return data;
    }

    public long getValue() {
        return data;
    }
}

这样在JDK1.8中是可以使用的

@Contended注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Contended {
    String value() default "";
}

Contended注解可以用于类型上和属性上,加上这个注解之后虚拟机会自动进行填充,从而避免伪共享。这个注解在Java8 ConcurrentHashMap、ForkJoinPool和Thread等类中都有应用。我们来看一下Java8中ConcurrentHashMap中如何运用Contended这个注解来解决伪共享问题。以下说的ConcurrentHashMap都是Java8版本。

注意:在Java8中提供了**@sun.misc.Contended来避免伪共享时,在运行时需要设置JVM启动参数-XX:-RestrictContended**否则可能不生效。

缓存行填充的威力


/**
 * 缓存行测试
 */
public class CacheLineTest {
    /**
     * 是否启用缓存行填充
     */
    private final boolean isDataPadding = false;
    /**
     * 正常定义的变量
     */
    private volatile long x = 0;
    private volatile long y = 0;
    private volatile long z = 0;
    /**
     * 通过缓存行填充的变量
     */
    private volatile VolatileData volatileDataX = new VolatileData(0);
    private volatile VolatileData volatileDataY = new VolatileData(0);
    private volatile VolatileData volatileDataZ = new VolatileData(0);

    /**
     * 循环次数
     */
    private final long size = 100000000;

    /**
     * 进行累加操作
     */
    public void accumulationX() {
        //计算耗时
        long currentTime = System.currentTimeMillis();
        long value = 0;
        //循环累加
        for (int i = 0; i < size; i++) {
            //使用缓存行填充的方式
            if (isDataPadding) {
                value = volatileDataX.accumulationAdd();
            } else {
                //不使用缓存行填充的方式 因为时单线程操作不需要加锁
                value = (++x);
            }


        }
        //打印
        System.out.println(value);
        //打印耗时
        System.out.println("耗时:" + (System.currentTimeMillis() - currentTime));
    }

    /**
     * 进行累加操作
     */
    public void accumulationY() {
        long currentTime = System.currentTimeMillis();
        long value = 0;
        for (int i = 0; i < size; i++) {
            if (isDataPadding) {
                value = volatileDataY.accumulationAdd();
            } else {
                value = ++y;
            }


        }
        System.out.println(value);
        System.out.println("耗时:" + (System.currentTimeMillis() - currentTime));
    }

    /**
     * 进行累加操作
     */
    public void accumulationZ() {
        long currentTime = System.currentTimeMillis();
        long value = 0;
        for (int i = 0; i < size; i++) {
            if (isDataPadding) {
                value = volatileDataZ.accumulationAdd();
            } else {
                value = ++z;
            }
        }
        System.out.println(value);
        System.out.println("耗时:" + (System.currentTimeMillis() - currentTime));
    }

    public static void main(String[] args) {
        //创建对象
        CacheLineTest cacheRowTest = new CacheLineTest();
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        //启动三个线程个调用他们各自的方法
        executorService.execute(() -> cacheRowTest.accumulationX());
        executorService.execute(() -> cacheRowTest.accumulationY());
        executorService.execute(() -> cacheRowTest.accumulationZ());
        executorService.shutdown();
    }
}

不使用缓存行填充测试

/**
  * 是否启用缓存行填充
  */
 private final boolean isDataPadding = false;

输出

100000000
耗时:7960
100000000
耗时:7984
100000000
耗时:7989

使用缓存行填充测试

/**
  * 是否启用缓存行填充
  */
 private final boolean isDataPadding = true;

输出

100000000
耗时:176
100000000
耗时:178
100000000
耗时:182

同样的结构他们之间差了 将近 50倍的速度差距

总结

​ 当多个线程同时对共享的缓存行进行写操作的时候,因为缓存系统自身的缓存一致性原则,会引发伪共享问题,解决的常用办法是将共享变量根据缓存行大小进行补充对齐,使其加载到缓存时能够独享缓存行,避免与其他共享变量存储在同一个缓存行。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/692567.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一图看懂CodeArts Board 5大特性,带你玩转看板服务

华为云看板服务CodeArts Board&#xff0c;通过构建研发效能度量体系&#xff0c;实现软件研发过程的可视化、软件交付的可管理可跟踪可量化&#xff0c;及时识别研发过程的堵塞点和改进点&#xff0c;通过数据驱动运营和治理&#xff0c;不断提升企业的软件能力和研发效能。

详解JAVA序列化

目录 1.什么是序列化 2.JAVA中的序列化 2.1.成员变量必须可序列化 2.2.transient关键字&#xff0c;可避免被序列化 2.3.无法更新状态 2.4.serialVersionUID 3.JDK序列化算法 4.序列化在实际中的一些应用 1.什么是序列化 序列化就是将对象转换为二进制格式的过程。对象…

Maven安装和配置详细教程

Maven安装和配置详细教程 1、Maven简介 Maven 是 Apache 软件基金会的一个开源项目,是一个优秀的项目构建工具,它用来帮助开发者管理项目中的 jar,以及 jar 之间的依赖关系、完成项目的编译、测试、打包和发布等工作。 2、Maven下载 点击Maven下载官方地址下载Maven。或者去…

postman持续集成-Jenkins自动构建

自动构建&#xff0c;就是设置一个定时器&#xff0c;定时时间到&#xff0c; Jenkins 自动执行测试用例 比如说,我设置下午五点,那么jenkins就是自动执行命令,自动生成报告,后续还可加上邮箱,会把报告发至邮箱 1. Jenkins 首页&#xff0c;点击任务名&#xff1a;如&#xff…

数据库—关系代数

传统的集合运算 在数据库中的关系代数运算有以下三种基本运算 并交差 必须满足两个表之间的属性个数必须一样。&#xff08;必须具有相容性&#xff09; 投影与选择运算 投影&#xff1a;π L _L L​( R ) 解释->π是投影符号&#xff0c;L是R表中的属性列&#xff0c;从…

临时文件中转服务的搭建-chfs软件的使用

因为经常用到远程桌面连接&#xff0c;所以本地pc和远程pc间的文件传输一直是个经常遇到的问题&#xff0c;尝试过用vftp搭建ftp服务&#xff0c;但是该服务在许多vps上被禁用&#xff0c;且windows上使用也要进行设置&#xff0c;比较麻烦。所幸发现了ods-im/CuteHttpFileServ…

接口测试如何做?你真的会做吗?全网超全整理实战案例...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 API测试的流程 准…

分布式事务Seate

一、Seata简介 1、Seata的核心组件 TC (Transaction Coordinator)事务协调者&#xff1a;维护全局和分支事务的状态&#xff0c;协调全局事务提交或回滚。TM (Transaction Manager)事务管理器&#xff1a;定义全局事务的范围、开始全局事务、提交或回滚全局事务。RM (Resourc…

2023下半年北京/上海/深圳软考(中/高级)认证招生

软考是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资格考试。 系统集成…

Docker安装与启动

Docker安装与启动 文章目录 Docker安装与启动前言容器与虚拟机比较 1、安装Docker2、设置ustc的镜像3、Docker的启动与停止总结 前言 容器与虚拟机比较 虚拟机&#xff08;VM&#xff09;是计算机系统的仿真。简而言之&#xff0c;它可以在实际上是一台计算机的硬件上运行看起…

Docker教程

Docker 能解决的问题 ⾸先&#xff0c;我们先来看⼏个问题&#xff1a; 1. 合作开发的时候&#xff0c;在本机可以运⾏&#xff0c;在别⼈的电脑上跑不起来。 这⾥我们以 Java Web 应⽤程序为例&#xff0c;⼀个 Java Web 应⽤程序涉及很多东⻄&#xff0c;⽐如 JDK 、 Tomc…

机器学习 day21(Tensorflow代码实现)

Tensorflow代码实现 在tensorflow中训练神经网络模型的步骤&#xff1a;第一步&#xff1a;指定模型&#xff0c;并告诉tensorflow按何种方式计算。第二步&#xff1a;使用特定的损失函数编译模型。第三步&#xff1a;训练模型

企业知识管理要怎么做,才能清晰有序?

在当今快速变化的商业环境中&#xff0c;企业知识管理的重要性日益凸显。有效的知识管理可以帮助企业整理、保存和传递知识&#xff0c;提高员工的工作效率和创新能力&#xff0c;从而为企业获得竞争优势奠定基础。本文将介绍企业在进行知识管理时应采取的措施&#xff0c;以确…

微型导轨的使用寿命能达到多久?

微型导轨&#xff0c;顾名思义就是体积很小的导轨&#xff0c;一般是应用在小型化设备中的&#xff0c;像半导体设备&#xff0c;医疗设备&#xff0c;IC制造设备&#xff0c;X-Y table&#xff0c;精密测量及检测仪器&#xff0c;高速皮带驱动设备&#xff0c;高速移载设备等都…

传说中,让测试猿分分钟心酸的五大谣言

谣传1&#xff1a;测试无聊 综观现今软件测试的一些轶事&#xff0c;我对某些错误想法的频繁出现感到吃惊。尽管有很多可以罗列&#xff0c;但是我还是想分享测试的五个最常见的谣传&#xff08;基于我短暂的经验&#xff09;。我发现前三个盛行于一些主流的新闻文章&#xff…

STM32F407系统时钟的配置和查看方法

1、系统时钟的来源 STM32F407具有两个PLL&#xff0c;用于产生不同的时钟信号。这里主要来讨论主PLL时钟。主PLL时钟的时钟源有两个信号&#xff0c;分别是上边提到的HIS信号和HSE信号。PLL通过把这两个信号倍频&#xff0c;分频等达到更高频率的时钟信号。一般来说&#xff0…

Linux6.yum,git,gdb

1.yum三板斧 yum list :显示所有能安装的软件。 yum lisy | grep 软件 :搜索软件。 yum install -y :安装软件。 yum remove -y 软件 :删除已经安装的软件。 2.git git clone 仓库网址 :添加仓库&#xff0c;按回车之后。需要输入账户和密码。 git add 文件 :把文件添加…

图像几何变换、仿射变换、透视变换

图像几何变换:平移、缩放、旋转 图像旋转变换:(x,y)为原图像坐标系,(x’,y’)为以(x0,y0)为中心的笛卡尔坐标系,图像以x0,y0)为中心进行旋转。 图像坐标系->笛卡尔坐标系->图像坐标系。如果是以图像中心旋转,则left=W/2,right=H/2,其中W和H为图像旋转…

MinGW 编译jsoncpp 的下载和编译

目录 1. jsoncpp 的下载 1.1 下载地址&#xff0c;点击可直接下载 1.2 下载完解压内容如下 2. jsoncpp 的编译 2.1 配置 2.2 生成 3. jsoncpp测试使用 1. jsoncpp 的下载 1.1 下载地址&#xff0c;点击可直接下载 mirrors / open-source-parsers / jsoncpp GitCode 1…

数据库监控与调优【十五】—— ORDER BY语句优化

ORDER BY语句优化 最好的做法&#xff1a;利用索引避免排序 实验 目的&#xff1a;哪些情况下ORDER BY子句能用索引避免排序&#xff0c;哪些情况下不能 之前说过&#xff0c;BTree数据结构里面的关键字&#xff0c;也就是索引值都是按照顺序排列的&#xff0c;那么&#xf…