JAVA中的伪共享与缓存行

这篇具有很好参考价值的文章主要介绍了JAVA中的伪共享与缓存行。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一.伪共享与缓存行

1.CPU缓存架构

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

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

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

java 伪共享,java,java

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

java 伪共享,java,java

 2.什么是伪共享

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

java 伪共享,java,java

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

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

java 伪共享,java,java

 文章来源地址https://www.toymoban.com/news/detail-518334.html

 如上图变量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对象的字段进行排序,如下表所示。

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倍的速度差距

总结

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

 

到了这里,关于JAVA中的伪共享与缓存行的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Android】细数Linux和Android系统中的伪文件系统

    做了好些年Android开发,你了解过Linux伪文件系统吗? 在 Linux 中,伪文件系统(Pseudo Filesystem)是一种特殊的文件系统,它不涉及具体的物理存储设备,而是提供了一种接口,允许用户和应用程序以文件和目录的形式访问内核和其他系统信息。这些文件和目录在运行时被内核动

    2024年01月24日
    浏览(22)
  • JavaScript前端中的伪类元素before和after使用详解

    在前端开发中,伪类是一种让你可以选择元素的某个状态或位置的 CSS 选择器。其中, :before 和 :after 伪类允许你在一个元素之前或之后插入内容。 :before 和 :after 伪类创建的元素是不在 HTML 文档中的,它们是通过 CSS 生成的。可以用它们来在一个元素的前面或后面插入一些内

    2024年02月14日
    浏览(30)
  • 合肥工业大学机器视觉期末复习 课件梳理(穿插作业中的伪代码)

    第一部分:低层次视觉 1、滤波器 2、梯度—边缘;梯度—能量(线裁剪) 3、模板匹配;二值图像分析 4、纹理 第二部分:中层次视觉 5、霍夫变换 6、分割 7、局部不变特征——检测、描述和匹配 8、立体 第三部分:高层次的视觉 9、实例识别 10、监督分类的对象检测 11、支持向

    2024年02月02日
    浏览(38)
  • 【java缓存、redis缓存、guava缓存】java中实现缓存的几种方式

    这种方式可以简单实现本地缓存,但是实际开发中不推荐使用,下面我们来实现一下这种方式。 首先创建一个管理缓存的类 这个类中有一个静态代码块,静态代码块会在类加载时就执行,我们可以在这里完成对缓存的初始化,决定缓存内一开始就有哪些数据 另外我们还可以

    2024年02月16日
    浏览(23)
  • Java扩展Nginx之七:共享内存

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 作为《Java扩展Nginx》系列的第七篇,咱们来了解一个实用工具 共享内存 ,正式开始之前先来看一个问题 在一台电脑上,nginx开启了多个worker,如下图,如果此时我们用了nginx-clojure,就相当于有了四

    2024年02月16日
    浏览(28)
  • 【Java】线程数据共享和安全 -ThreadLocal

     🎄欢迎来到@边境矢梦°的csdn博文🎄  🎄本文主要梳理线程数据共享和安全 -ThreadLocal🎄 🌈我是边境矢梦°,一个正在为秋招和算法竞赛做准备的学生🌈 🎆喜欢的朋友可以关注一下 🫰🫰🫰 ,下次更新不迷路🎆 Ps: 月亮越亮说明知识点越重要 (重要性或者难度越大)🌑🌒

    2024年02月09日
    浏览(32)
  • 【Java 进阶篇】Java Cookie共享:让数据穿越不同应用的时空隧道

    在Web开发中,Cookie是一种常见的会话管理技术,用于存储和传递用户相关的信息。通常,每个Web应用都会在用户的浏览器中设置自己的Cookie,以便在用户与应用之间保持状态。然而,有时我们需要在不同的应用之间共享Cookie数据,让数据像穿越时空的时光旅行一样在不同的

    2024年02月05日
    浏览(89)
  • 【Java 基础篇】Java网络编程实战:P2P文件共享详解

    Java网络编程是现代软件开发中不可或缺的一部分,因为它允许不同计算机之间的数据传输和通信。在本篇博客中,我们将深入探讨Java中的P2P文件共享,包括什么是P2P文件共享、如何实现它以及一些相关的重要概念。 P2P(Peer-to-Peer)文件共享是一种分布式计算模型,其中每个

    2024年02月07日
    浏览(27)
  • 【开源】基于JAVA的教学资源共享平台

    基于JAVA+Vue+SpringBoot+MySQL的教学资源共享平台,包含了课程管理、课程课件、授课中心、作业发布、课程评价、课程质量分析、交流互动模块,还包含系统自带的用户管理、部门管理、角色管理、菜单管理、日志管理、数据字典管理、文件管理、图表展示等基础模块,教学资源

    2024年01月19日
    浏览(45)
  • java+springboot+mysql大学图书共享交流平台

    项目介绍: 使用java+ssm+mysql开发的大学图书共享交流平台,系统包含超级管理员,系统管理员、用户角色,功能如下: 用户:主要是前台功能使用,包括注册、登录;查看图书交流(发布、查看、借阅、评论、收藏图书),系统留言,关于我们, 用户还具备个人中心功能:

    2024年02月15日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包