CPU的缓存
主内存是数据存放的地方,cpu处理速度与硬盘、内存的访问速度相差太大,因此添加cpu的缓存进行中间过渡,否则主内存的访问速度的严重影响cpu整体的运行速度
对一块数据多次运算,那么在执行运算的时候就会加载到离cpu比较近的就有了意义,
越靠近cpu的缓存运行速度越快。
所以 L1 缓存很小但很快,并且紧靠着在使用它的 CPU 内核。
L2 大一些,也慢一些,并且仍然只能被一个单独的 CPU 核使用。
L3 在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有 CPU 核共享。
最后,主存保存着程序运行的所有数据,它更大,更慢,由全部插槽上的所有 CPU 核共享。
当 CPU 执行运算的时候,它先去 L1 查找所需的数据,再去 L2,然后是 L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。
走得越远,运算耗费的时间就越长。
所以如果进行一些很频繁的运算,要确保数据在 L1 缓存中。
缓存行
现在cpu读取数据通常以一块连续的块为单位,即缓存行,所以通常情况下访问连续的存储的数据会比随机访问要快,因为数组在内存中是连续分配的,缓存行的大小通常是64字节(常用处理器的缓存行是 64 字节的,比较旧的处理器缓存行是 32 字节),这意味着即使只操作一个字节,cpu做少也要读取这个数据所在的连续64字节的数据,java的long类型是8字节,因此在一个缓存行中可以存储8个long类型的变量
一个 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的更新需要重新从内存中进行读取,这就是伪共享
伪共享案例
package com.edward.frame.disruptor;
// 多个线程,每个线程操作一个VolatileLong数组中的元素
// VolatileLong是否进行填充会影响最终结果
// 为填充时会产生伪共享问题,运行更慢,填充后不会
public class FalseShareTest {
public static void main(final String[] args) throws Exception {
VolatileLong volatileLong = new VolatileLong();
volatileLong.value1 = 1;
volatileLong.value2 = 1;
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
volatileLong.value1++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 100000000; i++) {
volatileLong.value2++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(System.currentTimeMillis() - start);
}
}
class VolatileLong {
public volatile long value1;
public long p1, p2, p3, p4, p5, p6; // 注释此行,时间会比较长
public volatile long value2;
}
如何避免伪共享
1.我们已经知道一个缓存行是64个字节,一个long类型是8个字节,可以添加7个long类型的变量
class VolatileLong {
public volatile long value1;
public long p1, p2, p3, p4, p5, p6; // 注释此行,时间会比较长
public volatile long value2;
}
2.使用 @sun.misc.Contended 注解(java8)
在需要的类型添加注解,以下两种效果一样
@sun.misc.Contended
class VolatileLong {
public volatile long value1;
}
@sun.misc.Contended
class VolatileLong {
public volatile long value1;
public long p1, p2, p3, p4, p5, p6;
}
注意:
@Contended 注解会增加目标实例大小,要谨慎使用。默认情况下,除了 JDK 内部的类,JVM 会忽略该注解。要应用代码支持的话,要设置 -XX:-RestrictContended=false,它默认为 true(意味仅限 JDK 内部的类使用)。当然,也有个 –XX: EnableContented 的配置参数,来控制开启和关闭该注解的功能,默认是 true,如果改为 false,可以减少 Thread 和 ConcurrentHashMap 类的大小。参加《Java性能权威指南》210 页。
案例:
/**
* 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);
}
/**
* To prevent GC optimizations for cleaning unused padded references
*/
public long sumPaddingToPreventOptimization() {
return p1 + p2 + p3 + p4 + p5 + p6;
}
}
在ConcurrentHashMap中的size中CounterCell中使用了@sun.misc.Contended注解
欢迎关注作者公众号交流以及投稿