伪共享 FalseSharing (CacheLine,MESI) 浅析以及Java里的解决方案

起因

在阅读百度的发号器 uid-generator 源码的过程中,发现了一段很奇怪的代码:

/**
 * Represents a padded {@link AtomicLong} to prevent the FalseSharing problem<p>
 *
 * The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:<br>
 * 64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)
 *
 * @author yutianbao
 */
public class PaddedAtomicLong extends AtomicLong {
    private static final long serialVersionUID = -3415778863941386253L;

    /** Padded 6 long (48 bytes) */
    public volatile long p1, p2, p3, p4, p5, p6 = 7L;

    /**
     * Constructors from {@link AtomicLong}
     */
    public PaddedAtomicLong() {
        super();
    }

    public PaddedAtomicLong(long initialValue) {
        super(initialValue);
    }

}

这里面有6个看上去毫无作用的volatile long变量(标红)。如果这是我自己写的代码,我肯定会认为是我自己手抖写多了。

但是作为百度的发号器,开源了这么久,如果是手抖早被fix了。肯定还是有深意的。于是阅读了一些类注释,看到了这句话:

to prevent the FalseSharing problem

果然,这几个变量不是毫无作用的,是为了解决FalseSharing问题。

但是转念一想,我好像不知道什么是FalseSharing?解决了一个问题,又陷入了另一个更大的问题。

于是就上网查了很多资料,阅读了很多博客,算是对FalseSharing有了一个初步的了解。在这里写出来也为了希望能帮到有同样困惑的人。

背景知识

要说清楚FalseSharing,不是一两句话能做到的事,有一些必须了解的背景知识需要补充一下。

计算机存储架构

上图展示的是不同层级的硬件和cpu之间的交互延迟。越靠近CPU,速度越快。

计算机运行时,CPU是执行指令的地方,而指令会需要一些数据的读写。程序的运行时数据都是存放在主存的,而主存又特别慢(相对),所以为了解决CPU和主存之间的速度差异,现代计算机都引入了高速缓存(L1L2L3)。

现代计算机对缓存/内存的设计一般如下:

L1和L2由CPU的每个核心独享,而L3则被整个CPU里所有核心共享(仅指单CPU架构)。

CPU访问数据时,按照先去L1,查不到去L2,再L3->主存的顺序来查找。

Cache Line

在上述CPU和缓存的数据交换过程中,并不是以字节为单位的。而是每次都会以Cache Line为单位来进行存取。

Cache Line其实就是一段固定大小的内存空间,一般为64字节。

MESI

这个东西研究过 volatile的同学可能会比较熟悉,这个就是各个告诉缓存之间的一个一致性协议。

因为L1 L2是每个核心自己使用,而不同核心又可能涉及共享变量问题,所以各个高速缓存间势必会有一致性的问题。MESI就是解决这些问题的一种方式。

MESI大致原理如下图:

我这里就摘抄一下网上搜到的解释:

在MESI协议中,每个Cache line有4个状态,可用2个bit表示,它们分别是:
M(Modified):这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中;
E(Exclusive):这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中;
S(Shared):这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中;
I(Invalid):这行数据无效。

通俗一点说,就是如果Core0和Core1都在使用一个共享变量变量A,则0,1都会在自己的Cache里有一份A的副本,分布在不同的CacheLine。

如果大家都没有修改A,则Core0和Core1里变量A所在的Cache Line的状态都是S。

如果Core0修改了A的值,则此时Core0的Cache Line变为M,Core1 的Cache Line变为I。

这样CPU就可以通过CacheLine的状态,来决定是删除缓存,还是直接读取什么的。

伪共享

背景知识介绍完毕了,这样再说伪共享就不会显得太难以理解了。

先说一个场景:

你的代码里需要使用一个volatile的Bool变量,当做多线程行为的一个开关:

static volatile boolean flag = true;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Integer count = 0;
                while (flag) {
                    ++count;
                    System.out.println(Thread.currentThread().getName() + ":" + count);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false;

        }).start();
    }

这段代码会声明一个flag为true,然后有10个工作线程会在flag为true时没100ms对count做个自增操作,然后输出。当flag为false时,就会结束线程。

还有一个线程A,会在1000ms后将flag置为false。

这里就是volatile的一个经典用法,可以保证多个线程对flag的可见性,不会因为线程A修改了flag的值,但是工作线程读取到的不是最新值而额外执行一些工作。

这段代码看起来是没有任何问题的,实际上跑起来也没有问题。

但是结合之前的背景知识,考虑一下flag所在的cache line,肯定还会有其他的变量(cache line 64字节,bool无法完整填充一个CacheLine)。

如果flag所在的CacheLine里还有一个频繁修改的共享变量,这时会发生什么?

很简单,就是flag所在的CacheLine被频繁置为不可用,需要清除缓存重新读取。flag在工作状态并没有被修改,但是仍然会被其他频繁修改的共享变量所影响。

这样就会带来一个问题,即使flag并没有被修改,但我们的工作线程很多时间都等于是在主存中读取flag的值,这样在高并发时会带来很大的效率问题。

以上就是所谓的 “FalseSharing” 问题。

解决办法

FalseSharing对于普通业务应用,基本没什么实际影响。但是对于很多超高并发的中间件(例如发号器),可能就会带来一定的性能瓶颈。所以这类项目都是需要关注这个问题的。

出现原因已经说清楚了,那么该如何解决呢?

其实答案就在文章的开头,那6个看上去没有任何含义的volatile long变量,就是用来解决这个问题的。

The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)

这行注释就说明了这6个变量是如何解决FalseSharing问题的:

CacheLine一般是64字节,64 = 8(对象本身的属性信息)+ 6*8(long占用8个字节) + 8 (AtomicLong本身带有一个long) 。

写了这6个看着无效的变量后,PaddedAtomicLong就会占用64个字节,正好填满一个CacheLine,这样就会被独自分配到一个CacheLine,这样就不存在FalseSharing问题了。

需要注意的是本来AtomicLong仅占用不到20字节,但是为了解决FalseSharing做了填充之后就占用64字节了,这样就会导致空间会膨胀很多。所以即使用的时候也要做好取舍。

原文地址:https://www.cnblogs.com/csonezp/p/11997194.html

时间: 2024-11-12 07:34:32

伪共享 FalseSharing (CacheLine,MESI) 浅析以及Java里的解决方案的相关文章

java 伪共享

MESI协议及RFO请求典型的CPU微架构有3级缓存, 每个核都有自己私有的L1, L2缓存. 那么多线程编程时, 另外一个核的线程想要访问当前核内L1, L2 缓存行的数据, 该怎么办呢?有人说可以通过第2个核直接访问第1个核的缓存行. 这是可行的, 但这种方法不够快. 跨核访问需要通过Memory Controller(见上一篇的示意图), 典型的情况是第2个核经常访问第1个核的这条数据, 那么每次都有跨核的消耗. 更糟的情况是, 有可能第2个核与第1个核不在一个插槽内.况且Memory C

伪共享和缓存行填充,从Java 6, Java 7 到Java 8

关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题.否则不仅无法发挥多线程的优势,还可能比单线程性能还差.随着JAVA版本的更新,再各个版本上减少伪共享的做法都有区别,一不小心代码可能就失效了,要注意进行测试.这篇文章总结一下. 什么是伪共享 关于伪共享讲解最清楚的是这篇文章<剖析Disruptor:为什么会这么快?(三)伪共享>,我这里就直接摘抄其对伪共享的解释: 缓存系统中是以缓存行(cache line)为单位存储的.缓存行是2的整数

从缓存行出发理解volatile变量、伪共享False sharing、disruptor

volatilekeyword 当变量被某个线程A改动值之后.其他线程比方B若读取此变量的话,立马能够看到原来线程A改动后的值 注:普通变量与volatile变量的差别是volatile的特殊规则保证了新值能马上同步到主内存,以及每次使用前能够马上从内存刷新,即一个线程改动了某个变量的值,其他线程读取的话肯定能看到新的值. 普通变量: 写命中:当处理器将操作数写回到一个内存缓存的区域时.它首先会检查这个缓存的内存地址是否在缓存行中,假设不存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不

伪共享(false sharing),并发编程无声的性能杀手

在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor,它被誉为“最快的消息框架”,其 LMAX 架构能够在一个线程里每秒处理 6百万 订单!在讲到 Disruptor 为什么这么快时,接触到了一个概念——伪共享( false sharing ),其中提到:缓存行上的写竞争是运行在 SMP 系统中并行线程实现可伸缩性最重要的限制因素.由于从代码中很难看

Disruptor的伪共享解决方案

1.术语 术语 英文单词 描述 内存屏障 Memory Barriers 是一组处理器指令,用于实现对内存操作的顺序限制. In the Java Memory Model a volatile field has a store barrier inserted after a write to it and a load barrier inserted before a read of it. 缓存行 Cache line 缓存中可以分配的最小存储单位.处理器填写缓存线时会加载整个缓存线,

多线程中的volatile和伪共享

  伪共享 false sharing,顾名思义,“伪共享”就是“其实不是共享”.那什么是“共享”?多CPU同时访问同一块内存区域就是“共享”,就会产生冲突,需要控制协议来协调访问.会引起“共享”的最小内存区域大小就是一个cache line.因此,当两个以上CPU都要访问同一个cache line大小的内存区域时,就会引起冲突,这种情况就叫“共享”.但是,这种情况里面又包含了“其实不是共享”的“伪共享”情况.比如,两个处理器各要访问一个word,这两个word却存在于同一个cache line

并发不得不说的伪共享

前言 可谓是一入并发深似海,看得越多,发现自己懂的越少,总感觉自己只是了解了其冰山一角.但是在研究的过程中越来越感受到一些框架的设计之美,很细腻的赶脚.同时也让我get到了新的知识点. CPU缓存 在正式进入正题之前,必须得先说说缓存这个概念.对于缓存这个概念相信大多数程序猿都不会很陌生,在大大小小项目中都会遇到.举个最简单的例子:数据一般都会存放到数据库之中.但在某些应用场景中不可能每次加载数据都去从数据库中加载(毕竟io操作是非常耗时和耗性能的),而是会用redis之类的缓存中间件去过渡,在

多线程之 Cache Line 与伪共享

Cache 简介 Cache,即缓存.缓存能提升读取性能,其原理是用性能更好的存储介质存储一部分高频访问的内容,获得总体概率上的速度提升. 在开发中,我们口中的缓存可以是一个变量,或者是 redis.在计算机 CPU 内部,往往指的是 CPU 的各级 cache. Cache 的一致性 由于是高频访问的内容被重复存储到了好几处地方,必然要考虑一致性.你需要及时清除或者更新缓存中过期内容.在程序设计中,使用缓存的架构通常是给定一个过期时间.而对于 CPU Cache,情况就复杂很多. CPU Ca

杂谈 什么是伪共享(false sharing)?

问题 (1)什么是 CPU 缓存行? (2)什么是内存屏障? (3)什么是伪共享? (4)如何避免伪共享? CPU缓存架构 CPU 是计算机的心脏,所有运算和程序最终都要由它来执行. 主内存(RAM)是数据存放的地方,CPU 和主内存之间有好几级缓存,因为即使直接访问主内存也是非常慢的. 如果对一块数据做相同的运算多次,那么在执行运算的时候把它加载到离 CPU 很近的地方就有意义了,比如一个循环计数,你不想每次循环都跑到主内存去取这个数据来增长它吧. 越靠近 CPU 的缓存越快也越小. 所以 L