起因

在阅读百度的发号器 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. 学习笔记之javascript编写简单计算器

      感觉自己的的实力真的是有待提高,在编写计算器的过程中,出现了各种各样的问题,暴露了自己的基础不扎实,逻辑思维能力不够,学得知识不能运用到自己的demo中区.先介绍一些这个这个计算器的整体思路.大致 ...

  2. windows上使用VsCode开发C/C++

    使用VsCode+makefile开发C/C++ 1. 介绍 vscode作为现在越来越受欢迎的编辑器之一,因为可以使用插件让vscode支持几乎市面上所有的编程语言,由于笔者主要接触的是 C/C++ ...

  3. [BZOJ4310] 跳蚤 SAM || SA

    没有代码的. 传送门 先二分出第 \(mid\) 大的字串 \(s\),然后从后往前切割,每次大于 \(s\) 了就不行. 涉及到的操作:求第 \(mid\) 大子串:比较两个字串(求 \(lcp\) ...

  4. NOIP模拟测试36考试反思

    这次考试..炸裂,归根到底是心态问题,考试的时候T1秒正解,但是调了三个小时死活没调出来,最后弃了,T2极水,没时间打了,T3题看都没看,总而言之就是不行. 教练说的对,一时强不一定一直强,心态其实也 ...

  5. 奶牛邻居——treap+契比雪夫距离+并查集

    题目描述 了解奶牛们的人都知道,奶牛喜欢成群结队.观察约翰的N(1≤N≤100000)只奶牛,你会发现她们已经结成了几个“群”. 每只奶牛在吃草的时候有一个独一无二的位置坐标Xi,Yi(l≤Xi,Yi ...

  6. ElasticSearch(一):基本概念

    ElasticSearch(一):基本概念 学习课程链接<Elasticsearch核心技术与实战> 基本概念示意图 索引与文档更偏向于开发人员的视角,属于逻辑上的一种概念:节点与分片更偏 ...

  7. js数组方法大全(上)

    # js数组方法大全(上) 记录一下整理的js数组方法,免得每次要找方法都找不到.图片有点多,注意流量,嘻嘻! 本期分享 join() reverse() sort() concat() slice( ...

  8. mysql双主互为主从配置

    在使用双主互为主从的模式前提是互为主从的两个数据库,表的主键必须是自增的. 环境不多说了 ,多台mysql单实例或单台多实例都可以.多实例的配置请参考:https://www.cnblogs.com/ ...

  9. 三张关联表,大表;单次查询耗时400s,有group by order by 如何优化

    问题SQL: select p.person_id as personId, p.person_name as personName, p.native_place as nativePlace, c ...

  10. 了解Spring的基本概念

    参考资料:https://www.jianshu.com/p/1c483bd8fd6d 在正式学习Spring框架之前,肯定有很多疑问,比如说: 1.Spring中经常出现的IOC.AOP.DI是什么 ...