Disruptor笔记(一)-预备知识

Memory Barrier 内存障

.它是一个CPU指令。是的,再一次,我们在思考CPU级的东西以便得到我们需要的性能(Martin著名的MechanicalSympathy)。基本上它是一个指令,为了a)确保特定运算的执行顺序和b)影响一些数据(可能是一些指令的执行结果)的可见性。

Disruptor笔记(一)-预备知识_第1张图片

.编译器和CPU能对指令重新排序,来尝试优化性能,最终执行结果是一样的。插入一个内存障会告诉CPU和编译器在那个命令之前执行的需要呆在那个命令之前,在那个命令之后执行的需要呆在那之后。就像一次去拉斯维加斯的旅游全在你脑子里一样。

.内存障做的另一件事是强制各种CPU缓存的更新-比如,一个写障会把在这个障之前写到缓存的数据全刷新,于是其他任何线程去读那个数据都会拿到最新的版本,不管它是由哪个内核或socket执行的。

Disruptor笔记(一)-预备知识_第2张图片

.在java中,这里神奇的咒语是单词"volatile"(一个我觉得在Java认证中从没明确地解释过的东西)。如果你的字段是volatile的,Java内存模型会在你对它写入之后插入一个写障指令,并且在你对它读取之前插入一个读障指令。

这意味着如果你对一个volatile字段写入,你知道的:

  .任何在你对这个字段写入之后访问它的线程都会得到更新后的值。

  .任何你在对这个字段写入之前做的事都被确保发生过了,而任何更新过的数据值都会变得可见,因为内存障把所有早先对缓存的写入都刷新了。


cache line padding 高速缓存行 补齐

内存缓存系统中基本单元是高速缓存行(Cache lines). cpu会把数据从内存加载到高速缓存中 ,这样可以获得更好的性能,高速缓存默认大小是64 Byte为一个区域,一个区域在一个时间点只允许一个核心操作,也就是说不能有多个核心同时操作一个缓存区域。


因为高速缓存是64字节,而Hotspot JVM的对象头是两个部分组成,第一部分是由24字节的hash code和8字节的锁等状态标识组成,第二部分是指向该对象类的引用。基本类型字节如下:
doubles (8) and longs (8)
ints (4) and floats (4)
shorts (2) and chars (2)
booleans (1) and bytes (1)
references (4/8)


False Share   伪内存

例子:

设想你的long数据不是数组的一部分。设想它只是单独的一个变量。让我们称它为“头”,没什么理由。然后再设想在你的类中有另一个变量紧挨着它。让我们直接称它为“尾”。现在,当你加载"头"到高速缓存的时候,你免费加载了“尾”。


Disruptor笔记(一)-预备知识_第3张图片


直到你意识到“尾”正在被你的生产者写入,而“头”正在被你的消费者写入。这两个变量实际上并不是密切相关的,而且事实上是要被两个可能在两个不同的内核中运行的不同线程使用的。

Disruptor笔记(一)-预备知识_第4张图片

设想你的消费者更新了“头”。缓存中的值被更新了,内存中的值被更新了,而其他任何缓存块中存在的“头”都失效了因为其他缓存不会有崭新的值。记住我们是在整个块级处理的,我们没法只把“头”标记为无效。


Disruptor笔记(一)-预备知识_第5张图片

现在如果一些进程正在另一个内核上运行,只是想读“尾”的值,整个缓存块需要被重新从主内存读取。那么一个和你的消费者无关的线程读一个和“头”无关的值,它被高速缓存未命中给拖慢了。

当然如果两个独立的线程同时对那两个值写入会更糟。每次当另一个线程对高速缓存块做了写操作的时候,每个内核都要把另一个内核上的高速缓存块失效掉并重新读取里面的数据。你基本上是遇到两个线程之间的写冲突了尽管它们写入的是不同的变量。

这叫作“伪共享”,因为每次你访问“头”你都会得到“尾”,而且每次你访问“尾”,你同样会得到“头”。这一切都在后台发生,没有任何编译警告会告诉你你正在写一个并发访问效率很低的代码。

你可能感兴趣的:(Disruptor笔记(一)-预备知识)