彻底搞清楚什么是伪共享

1、什么是伪共享?

1.1背景


1.1.1 CPU缓存架构


我们知道CPU的处理速度与内存、硬盘的访问速度有很到的关系,为了缓解CPU处理速度与内存、硬盘的访问速度的差别,在当代的CPU中,普通引入了CPU缓存,那CPU缓存的架构是怎样的,直接上图:

当代CPU架构基本都是三层架构,这是基于访问速度和容量成本做出的权衡,从图中可以看出:

从L1到L3到RAM,访问速度相对变慢,存储变大

L1、L2基本都是被单核独享,L3被插槽上的CPU的所有核共享

当CPU运算需要数据时,先从L1上要,L1上没有就到L2上要,L2上没有就到L3上要,直到Ram,硬盘,越远就越耗时:


1.1.2缓存行cache line

CPU缓存是有缓存行组成的,一个缓存行一般是64个字节,CPU读取数据是以缓存行为单位的读取,这意味着即使是读1个字节的数据,CPU也要读取这个数据所在的连续的64个字节的数据,如果使用的数据结构中的数据项不是彼此相邻连续的,如链表,那么读数据的时候就得不到免费缓存带来的好处,在java中,数组中的数据通常是连续的(数组的连续存储不是jvm规范中的要求,在某些jvm中,大中型数据项不是分配在连续的空间),所以数组的访问速度比链表要快。

1.1.3 缓存失效

在java中,long类型占8个字节,这就意味着,当读一个long类型的变量,也会读取其相邻的7个long类型变量(不是long类型的变量按占用的字节数计算个数,如int类型的变量占4个字节,那么就64-8个字节,就可以存储14个int类型的变量),在基于mesi协议下,其它的线程此时再读取其中的一个long类型变量,那么这个long类型变量所在的缓存行就会失效,需要重新读取缓存,这就是缓存失效。

1.2 伪共享

CPU在读取数据时,是以一个缓存行为单位读取的,假设这个缓存行中有两个long类型的变量a、b,当一个线程A读取a,并修改a,线程A在未写回缓存之前,另一个线程B读取了b,读取的这个b所在的缓存是无效的(前面说的缓存失效),本来是为了提高性能是使用的缓存,现在为了提高命中率,反而被拖慢了,这就是传说中的伪共享。

1.3 那么如何消除伪共享呢?

当多线程修改相互独立的变量时,如果这些变量在同一个缓存行,就会无意中影响彼此的性能,这个就是伪共享,那么我们怎么消除呢?

我们先看一个因为有伪共享而影响性能的例子:

/**

* 伪共享演示

*/

public class FalseSharingDemo {

public static void main(String[] args)throws InterruptedException {

        testPointer(new Pointer());

    }

private static void testPointer(Pointer pointer)throws InterruptedException {

long start = System.currentTimeMillis();

        Thread t1 =new Thread(() -> {

for (int i =0; i <100000000; i++) {

pointer.a++;

            }

}, "A");

        Thread t2 =new Thread(() -> {

for (int i =0; i <100000000; i++) {

pointer.b++;

            }

}, "B");

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println(System.currentTimeMillis() - start);

        System.out.println(pointer.a +"@" + Thread.currentThread().getName());

        System.out.println(pointer.b +"@" + Thread.currentThread().getName());

    }

}

class Pointer {

//在一个缓存行中,先会存储a

    volatile long a;

    //    放开下面这行,解决伪共享的问题,提供了性能  --- 方法1

    long p1, p2, p3, p4, p5, p6, p7;

    volatile long b;

}

程序输出:

3370   //基本在3000ms多的耗时

100000000@main

100000000@main

1.4解决伪共享的方法

知道了伪共享产生的原理,就不难找到解决伪共享的方法了,总结起来大概有下面三个

1.4.1 在变量的后背凑齐64个字节的变量

如上面的pointer类改成:

classPointer {

    //在一个缓存行中,先会存储a

volatile long a; //需要volatile,保证线程间可见并避免重排序//    放开下面这行,解决伪共享的问题,提高了性能

    long p1, p2, p3, p4, p5, p6, p7;

volatile long b;  //需要volatile,保证线程间可见并避免重排序

}

再运行上面的程序,输出:

1284  //基本在1000ms多的耗时,性能提高了2倍

100000000@main

100000000@main


1.4.2 使用消除了伪共享结构的类

如上面的程序不直接使用long类型,我们自动以一个long类型:MyLong

classPointer2{

    MyLong

a = newMyLong();

    MyLong b = newMyLong();

}

classMyLong {


volatile longvalue;

    longp1,p2,p3,p4,p5,p6,p7;

}

让后再简单的改下:

private static void testPointer(Pointer2 pointer)

pointer.a++ 改成 pointer.a.value++

pointer.b++ 改成 pointer.b.value++

再次执行程序,输出:

1285  //与不消除伪共享前,性能提高2倍

com.javastack.mtc.cacheline.MyLong@1de0aca6@main

com.javastack.mtc.cacheline.MyLong@255316f2@main



1.4.3 使用jdk8注解:@sun.misc.Contended

修改 MyLong 如下:

@sun.misc.Contended

classMyLong {

    volatile longvalue;

}

或者:

classPointer {

volatile long a;

    @Contended

volatile long b;

}

@Contended注解使用方法

@Contended 注解会增加目标实例大小,要谨慎使用。默认情况下,除了 JDK 内部的类,JVM 会忽略该注解。要应用代码支持的话,需要在jvm启动参数上设置 -XX:-RestrictContended=false,它默认为 true(意味仅限 JDK 内部的类使用)。当然,也有个 –XX: EnableContented 的配置参数,来控制开启和关闭该注解的功能,默认是 true,如果改为 false,可以减少 Thread 和 ConcurrentHashMap 类的大小。参考《Java性能权威指南》。


1.3 使用了伪共享的大牛例子?

1.3.1 jdk中的ConcurrentHashMap类

@sun.misc.Contended static final class CounterCell {
   volatile long value;
   CounterCell(long x) { value = x; }
}


1.3.2 jdk8中LongAdder的父类Striped64

// Striped64中的内部类Cell,使用@sun.misc.Contended注解,说明里面的值消除了伪共享

1.3.2 著名的disruptor中的ringBuffer


1.5 小结

CPU具有多级缓存,越接近CPU的缓存越小也越快;

CPU缓存中的数据是以缓存行为单位处理的;

CPU缓存行能带来免费加载数据的好处,所以处理数组性能非常高;

CPU缓存行也带来了弊端,多线程处理不相干的变量时会相互影响,也就是伪共享;

避免伪共享的主要思路就是让不相干的变量不要出现在同一个缓存行中;

一是每两个变量之间加七个 long 类型;

二是创建自己的 long 类型,而不是用原生的;

三是使用 java8 提供的注解;


更多干货在公号【java栈长】

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352