从缓存行出发理解volatile变量伪共享False sharingdisruptor

Posted mthoutai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从缓存行出发理解volatile变量伪共享False sharingdisruptor相关的知识,希望对你有一定的参考价值。

volatilekeyword

当变量被某个线程A改动值之后。其他线程比方B若读取此变量的话,立马能够看到原来线程A改动后的值

 

注:普通变量与volatile变量的差别是volatile的特殊规则保证了新值能马上同步到主内存,以及每次使用前能够马上从内存刷新,即一个线程改动了某个变量的值,其他线程读取的话肯定能看到新的值。

普通变量:

写命中:当处理器将操作数写回到一个内存缓存的区域时。它首先会检查这个缓存的内存地址是否在缓存行中,假设不存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中。

术语

英文单词

描写叙述

共享变量

 

在多个线程之间可以被共享的变量被称为共享变量。

共享变量包含全部的实例变量。静态变量和数组元素。

他们都被存放在堆内存中。Volatile仅仅作用于共享变量。

内存屏障

Memory Barriers

是一组处理器指令。用于实现对内存操作的顺序限制。

备注: In the Java Memory Model a volatile field has a store barrier inserted after a write to it and a load barrier inserted before a read of it.

缓冲行

Cache line

缓存中能够分配的最小存储单位。处理器填写缓存线时会载入整个缓存线。须要使用多个主内存读周期。

原子操作

Atomic operations

不可中断的一个或一系列操作。

缓存行填充

cache line fill

当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存(L1。L2,L3的或全部)

缓存命中

cache hit

假设进行快速缓存行填充操作的内存位置仍然是下次处理器訪问的地址时。处理器从缓存中读取操作数。而不是从内存。

写命中

write hit

当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中。假设不存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中。

写缺失

write misses the cache

一个有效的缓存行被写入到不存在的内存区域。

单核CPU缓存结构

 

 

单核CPU缓存

多核CPU缓存


所谓缓存航就是缓存中能够分配的最小存储单位。

处理器填写缓存线时会载入整个缓存线。须要使用多个主内存读周期。

以下降到伪缓存时会介绍,多核CPU、内存的缓存系统;

Information transfer between the cache and the memory is in terms of complete cache lines, rather than individual bytes. Thus if the program needs a particular byte, the entire cache line containing that byte is obtained from the memory. For example, suppose that the cache of Figure 2 was being used and the program fetches the word (two bytes) at location 0004736. If none of the cache lines contain the 16 bytes stored in addresses 0004730 through 000473F, then these 16 bytes are transferred from the memory to one of the cache lines. Because of the spatial locality of the program, we expect that other values in the cache line thus loaded will be referenced in the near future.

Volatile的实现原理

 

那么Volatile是怎样来保证可见性的呢?在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU会做什么事情。

 

Java代码:

instance = new Singleton();//instance是volatile变量

汇编代码:

0x01a3de1d: movb $0x0,0x1104800(%esi);

0x01a3de24: lock addl $0x0,(%esp);

 

 

 

有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发人员手冊可知。lock前缀的指令在多核处理器下会引发了两件事情


  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其它CPU里缓存了该内存地址的数据无效。
关键点:事实上相当于线程 像缓存行写数据的时候,会锁住缓存行,是其它线程不能读。写完后失效缓存行,其它线程便能够从内存读到共享变量的最新值了;

深度解析:
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其它)后再进行操作。但操作完之后不知道何时会写到内存;假设对声明了Volatile变量进行写操作。JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。可是就算写回到内存。假设其它处理器缓存的值还是旧的。再运行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。每一个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行相应的内存地址被改动,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行改动操作的时候,会强制又一次从系统内存里把数据读到处理器缓存里。

Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在运行指令期间。声言处理器的 LOCK# 信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间。处理器能够独占使用不论什么共享内存。

(由于它会锁住总线。导致其它CPU不能訪问总线,不能訪问总线就意味着不能訪问系统内存),可是在近期的处理器里,LOCK#信号一般不锁总线,而是锁缓存。毕竟锁总线开销比較大。在8.1.4章节有具体说明锁定操作对处理器缓存的影响。对于Intel486和Pentium处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和近期的处理器中,假设訪问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保改动的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同一时候改动被两个以上处理器缓存的内存区域数据

一个处理器的缓存回写到内存会导致其它处理器的缓存无效

IA-32处理器和Intel 64处理器使用MESI(改动,独占,共享,无效)控制协议去维护内部缓存和其它处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32 和Intel 64处理器能嗅探其它处理器訪问系统内存和它们的内部缓存。它们使用嗅探技术保证它的内部缓存,系统内存和其它处理器的缓存的数据在总线上保持一致。

比如在Pentium和P6 family处理器中。假设通过嗅探一个处理器来检測其它处理器打算写内存地址。而这个地址当前处理共享状态,那么正在嗅探的处理器将无效它的缓存行,在下次訪问同样内存地址时,强制运行缓存行填充。

Volatile的使用优化

著名的Java并发编程大师Doug lea在JDK7的并发包里新增一个队列集合类LinkedTransferQueue,他在使用Volatile变量时。用一种追加字节的方式来优化队列出队和入队的性能。

追加字节能优化性能?这样的方式看起来非常奇妙,但假设深入理解处理器架构就能理解当中的奥秘。

让我们先来看看LinkedTransferQueue这个类。它使用一个内部类类型来定义队列的头队列(Head)和尾节点(tail)。而这个内部类PaddedAtomicReference相对于父类AtomicReference仅仅做了一件事情,就将共享变量追加到64字节。

我们能够来计算下,一个对象的引用占4个字节。它追加了15个变量共占60个字节,再加上父类的Value变量,一共64个字节。


为什么追加64字节可以提高并发编程的效率呢? 由于对于英特尔酷睿i7,酷睿, Atom和NetBurst, Core Solo和Pentium M处理器的L1,L2或L3缓存的快速缓存行是64个字节宽。不支持部分填充缓存行,这意味着假设队列的头节点和尾节点都不足64字节的话。处理器会将它们都读到同一个快速缓存行中,在多处理器下每一个处理器都会缓存相同的头尾节点,当一个处理器试图改动头接点时会将整个缓存行锁定。那么在缓存一致性机制的作用下。会导致其它处理器不能訪问自己快速缓存中的尾节点,而队列的入队和出队操作是须要不停改动头接点和尾节点。所以在多处理器的情况下将会严重影响到队列的入队和出队效率。Doug lea使用追加到64字节的方式来填满快速缓冲区的缓存行。避免头接点和尾节点载入到同一个缓存行,使得头尾节点在改动时不会互相锁定。

那么是不是在使用Volatile变量时都应该追加到64字节呢?不是的。在两种场景下不应该使用这样的方式。第一:缓存行非64字节宽的处理器,如P6系列和奔腾处理器,它们的L1和L2快速缓存行是32个字节宽。第二:共享变量不会被频繁的写

由于使用追加字节的方式须要处理器读取很多其它的字节到快速缓冲区,这本身就会带来一定的性能消耗,共享变量假设不被频繁写的话。锁的几率也很小,就不是必需通过追加字节的方式来避免相互锁定。

/** head of the queue */
    private transient final PaddedAtomicReference < QNode > head;
 
    /** tail of the queue */
 
    private transient final PaddedAtomicReference < QNode > tail;
 
 
    static final class PaddedAtomicReference < T > extends AtomicReference < T > {
 
        // enough padding for 64bytes with 4byte refs
        Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
 
        PaddedAtomicReference(T r) {
 
            super(r);
 
        }
 
    }
 
    public class AtomicReference < V > implements java.io.Serializable {
 
        private volatile V value;

(c)2006-2024 SYSTEM All Rights Reserved IT常识