起因

在阅读百度的发号器 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字节了,这样就会导致空间会膨胀很多。所以即使用的时候也要做好取舍。

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

  1. 多线程伪共享FalseSharing

    1. 伪共享产生: 在SMP架构的系统中,每个CPU核心都有自己的cache,当多个线程在不同的核心上,并且某线程修改了在同一个cache line中的数据时,由于cache一致性原则,其他核心cac ...

  2. java高并发核心要点|系列5|CPU内存伪共享

    上节提到的:伪共享,今天我们来说说. 那什么是伪共享呢? 这得从CPU的缓存结构说起.以下如图,CPU一般来说是有三级缓存,1 级,2级,3级,越上面的,越靠近CPU的,速度越快,成本也越高.也就是说 ...

  3. Java 中的伪共享详解及解决方案

    1. 什么是伪共享 CPU 缓存系统中是以缓存行(cache line)为单位存储的.目前主流的 CPU Cache 的 Cache Line 大小都是 64 Bytes.在多线程情况下,如果需要修改 ...

  4. java 伪共享

    MESI协议及RFO请求典型的CPU微架构有3级缓存, 每个核都有自己私有的L1, L2缓存. 那么多线程编程时, 另外一个核的线程想要访问当前核内L1, L2 缓存行的数据, 该怎么办呢?有人说可以 ...

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

    关于伪共享的文章已经很多了,对于多线程编程来说,特别是多线程处理列表和数组的时候,要非常注意伪共享的问题.否则不仅无法发挥多线程的优势,还可能比单线程性能还差.随着JAVA版本的更新,再各个版本上减少 ...

  6. java中伪共享问题

    伪共享(False Sharing) 原文地址:http://ifeve.com/false-sharing/ 作者:Martin Thompson  译者:丁一 缓存系统中是以缓存行(cache l ...

  7. 线程基础:多任务处理——MESI协议以及带来的问题:伪共享

    1.概述 本文和后续文章将着眼CPU的工作原理阐述伪共享的解决方法和volatile关键字的应用. 2.复习CPU工作原理2.1.CPU工作原理要清楚理解本文后续内容,就需要首先重新概述一下JVM的内 ...

  8. 从Java视角理解CPU缓存和伪共享

    转载自:http://ifeve.com/from-javaeye-cpu-cache/               http://ifeve.com/from-javaeye-false-shari ...

  9. 关于java中的伪共享的认识和解决

    在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素: CPU缓存 网页浏览器为了加快速度,会在本机存缓存以前浏览过 ...

随机推荐

  1. C语言I作业06

    问题 答案 这个作业属于那个课程 C语言程序设计II 这个作业要求在哪里 https://edu.cnblogs.com/campus/zswxy/CST2019-4/homework/9888 我在 ...

  2. [考试反思]0924csp-s模拟测试51:破碎

    总参赛人数:15 有点菜. 不知道是撞了什么大运没有滚出A层. 但是一回到A层就暴露出了一个大问题:码速. 不是调试速度,,就是纯粹码的速度... 边讲考试状态边说吧... 上来肝T1.一看,是个换根 ...

  3. 使用Typescript重构axios(二十二)——请求取消功能:收尾

    0. 系列文章 1.使用Typescript重构axios(一)--写在最前面 2.使用Typescript重构axios(二)--项目起手,跑通流程 3.使用Typescript重构axios(三) ...

  4. NOIP原题 斗地主(20190804)

    题目描述 牛牛最近迷上了一种叫斗地主的扑克游戏.斗地主是一种使用黑桃.红心.梅花.方片的A到K加上大小王的共54张牌来进行的扑克牌游戏.在斗地主中,牌的大小关 系根据牌的数码表示如下:3<4&l ...

  5. 大数据之路day01_2--记事本与EditPlus编写Hello World并且运行

    在上一节我们成功的安装JAVA并且将其环境配置成功,接下来我们来编写第一个java程序——Hello World 1.利用记事本编写代码,利用命令行来编译运行 (1)新建记事本,(文件名).java后 ...

  6. python之变量名

    在python中,变量不需要提前声明,创建时直接对其赋值即可,变量类型由赋给变量的值决定.值得注意的是,一旦创建了一个变量,就需要给该变量赋值. 变量名应严格遵守以下规则: 1.变量名只能包含:字母/ ...

  7. 01-MyBatis启动流程分析

    目录 MyBatis简单介绍 启动流程分析 简单总结 附录 MyBatis内置别名转换 参考 MyBatis简单介绍 MyBatis是一个持久层框架,使用简单,学习成本较低.可以执行自己手写的SQL语 ...

  8. 易初大数据 2019年11月2日 计算机英语 wangqingchao

    一.Match the explantions in Column B with words and expressions in Column A.(搭配每组中同意以的词或短语) 1.交换机(pos ...

  9. PHP根据ip获取地理位置(通过高德地图接口)

    PHP根据ip获取地理位置(通过高德地图接口)<pre>//restapi.amap.com/v3/ip?key=2004f145cf3a39a72e9ca70ca4b2a1dc& ...

  10. docker showdoc安装

    自动脚本安装 前言 自动脚本脚本利用docker来安装运行环境,适用于linux服务器.如果你的服务器没有docker服务,脚本会尝试安装之.安装docker的过程可能有些慢.如果你已经安装过dock ...