(1)什么是 CPU 缓存行?java
(2)什么是内存屏障?数组
(3)什么是伪共享?缓存
(4)如何避免伪共享?数据结构
CPU 是计算机的心脏,全部运算和程序最终都要由它来执行。多线程
主内存(RAM)是数据存放的地方,CPU 和主内存之间有好几级缓存,由于即便直接访问主内存也是很是慢的。架构
若是对一块数据作相同的运算屡次,那么在执行运算的时候把它加载到离 CPU 很近的地方就有意义了,好比一个循环计数,你不想每次循环都跑到主内存去取这个数据来增加它吧。jvm
越靠近 CPU 的缓存越快也越小。源码分析
因此 L1 缓存很小但很快,而且紧靠着在使用它的 CPU 内核。性能
L2 大一些,也慢一些,而且仍然只能被一个单独的 CPU 核使用。学习
L3 在现代多核机器中更广泛,仍然更大,更慢,而且被单个插槽上的全部 CPU 核共享。
最后,主存保存着程序运行的全部数据,它更大,更慢,由所有插槽上的全部 CPU 核共享。
当 CPU 执行运算的时候,它先去 L1 查找所需的数据,再去 L2,而后是 L3,最后若是这些缓存中都没有,所需的数据就要去主内存拿。
走得越远,运算耗费的时间就越长。
因此若是进行一些很频繁的运算,要确保数据在 L1 缓存中。
缓存是由缓存行组成的,一般是 64 字节(经常使用处理器的缓存行是 64 字节的,比较旧的处理器缓存行是 32 字节),而且它有效地引用主内存中的一块地址。
一个 Java 的 long 类型是 8 字节,所以在一个缓存行中能够存 8 个 long 类型的变量。
在程序运行的过程当中,缓存每次更新都从主内存中加载连续的 64 个字节。所以,若是访问一个 long 类型的数组时,当数组中的一个值被加载到缓存中时,另外 7 个元素也会被加载到缓存中。
可是,若是使用的数据结构中的项在内存中不是彼此相邻的,好比链表,那么将得不到免费缓存加载带来的好处。
不过,这种免费加载也有一个坏处。设想若是咱们有个 long 类型的变量 a,它不是数组的一部分,而是一个单独的变量,而且还有另一个 long 类型的变量 b 紧挨着它,那么当加载 a 的时候将免费加载 b。
看起来彷佛没有什么毛病,可是若是一个 CPU 核心的线程在对 a 进行修改,另外一个 CPU 核心的线程却在对 b 进行读取。
当前者修改 a 时,会把 a 和 b 同时加载到前者核心的缓存行中,更新完 a 后其它全部包含 a 的缓存行都将失效,由于其它缓存中的 a 不是最新值了。
而当后者读取 b 时,发现这个缓存行已经失效了,须要从主内存中从新加载。
请记住,咱们的缓存都是以缓存行做为一个单位来处理的,因此失效 a 的缓存的同时,也会把 b 失效,反之亦然。
这样就出现了一个问题,b 和 a 彻底不相干,每次却要由于 a 的更新须要从主内存从新读取,它被缓存未命中给拖慢了。
这就是传说中的伪共享。
好了,上面介绍完CPU的缓存架构及缓存行机制,下面进入咱们的正题——伪共享。
当多线程修改互相独立的变量时,若是这些变量共享同一个缓存行,就会无心中影响彼此的性能,这就是伪共享。
咱们来看看下面这个例子,充分说明了伪共享是怎么回事。
public class FalseSharingTest { 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.x++; } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100000000; i++) { pointer.y++; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(System.currentTimeMillis() - start); System.out.println(pointer); } } class Pointer { volatile long x; volatile long y; }
这个例子中,咱们声明了一个 Pointer 的类,它包含 x 和 y 两个变量(必须声明为volatile,保证可见性,关于内存屏障的东西咱们后面再讲),一个线程对 x 进行自增1亿次,一个线程对 y 进行自增1亿次。
能够看到,x 和 y 彻底没有任何关系,可是更新 x 的时候会把其它包含 x 的缓存行失效,同时也就失效了 y,运行这段程序输出的时间为3890ms
。
伪共享的原理咱们知道了,一个缓存行是 64 个字节,一个 long 类型是 8 个字节,因此避免伪共享也很简单,笔者总结了下大概有如下三种方式:
(1)在两个 long 类型的变量之间再加 7 个 long 类型
咱们把上面的Pointer改为下面这个结构:
class Pointer { volatile long x; long p1, p2, p3, p4, p5, p6, p7; volatile long y; }
再次运行程序,会发现输出时间神奇的缩短为了695ms
。
(2)从新建立本身的 long 类型,而不是 java 自带的 long
修改Pointer以下:
class Pointer { MyLong x = new MyLong(); MyLong y = new MyLong(); } class MyLong { volatile long value; long p1, p2, p3, p4, p5, p6, p7; }
同时把 pointer.x++;
修改成 pointer.x.value++;
,把 pointer.y++;
修改成 pointer.y.value++;
,再次运行程序发现时间是724ms
。
(3)使用 @sun.misc.Contended 注解(java8)
修改 MyLong 以下:
@sun.misc.Contended class MyLong { volatile long value; }
默认使用这个注解是无效的,须要在JVM启动参数加上-XX:-RestrictContended
才会生效,,再次运行程序发现时间是718ms
。
注意,以上三种方式中的前两种是经过加字段的形式实现的,加的字段又没有地方使用,可能会被jvm优化掉,因此建议使用第三种方式。
(1)CPU具备多级缓存,越接近CPU的缓存越小也越快;
(2)CPU缓存中的数据是以缓存行为单位处理的;
(3)CPU缓存行能带来免费加载数据的好处,因此处理数组性能很是高;
(4)CPU缓存行也带来了弊端,多线程处理不相干的变量时会相互影响,也就是伪共享;
(5)避免伪共享的主要思路就是让不相干的变量不要出如今同一个缓存行中;
(6)一是每两个变量之间加七个 long 类型;
(7)二是建立本身的 long 类型,而不是用原生的;
(8)三是使用 java8 提供的注解;
java中有哪些类避免了伪共享的干扰呢?
还记得咱们前面介绍过的 ConcurrentHashMap 的源码解析吗?
里面的 size() 方法使用的是分段的思想来构造的,每一个段使用的类是 CounterCell,它的类上就有 @sun.misc.Contended 注解。
不知道的能够关注个人公众号“彤哥读源码”查看历史消息找到这篇文章看看。
除了这个类,java中还有个 LongAdder 也使用了这个注解避免伪共享,下一章咱们将一块儿学习 LongAdder 的源码分析,敬请期待。
你还知道哪些避免伪共享的应用呢?
欢迎关注个人公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一块儿畅游源码的海洋。