多线程“可见性”保证——volatile的应用

1. 引言

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见行”,可见性的意思是,当一个线程修改一个共享变量,另一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文切换和调度。

2. CPU缓存行

2.1 CPU常用术语

术语 英文 描述
内存屏障 memory barriers 是一组处理器指令,用于实现对内存操作的顺序限制
缓存行 cache line CPU高速缓存中可以分配的最小存储单位,缓存行是2的整数幂个连续字节,最常见的是64个字节,高速缓存中加载和修改数据都是以缓存行为基本单位进行处理的
原子操作 atomic operations 不可中断的一个或一系列操作
缓存行填充 cache line fill 将主存中的数据缓存到缓存行中
缓存命中 cache hit 处理器从缓存中读取到数据,而不是从主存中读取
写命中 write hit 当处理器操作数写回到一个内存缓存区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中

2.2 详解CPU缓存行

如果要了解缓存,就必须要了解缓存的结构,以及多个CPU核心访问缓存存在的一些问题和注意事项。
每个缓存里面都是由缓存行组成的,缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
在这里插入图片描述
需要注意,数据在缓存中不是以独立的项来存储的,cache是由缓存行组成的,通常是64字节(比较旧的处理器缓存行是32字节),并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。
在这里插入图片描述

2.3 缓存行的带来的好处

如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个。因此你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。
因此如果你数据结构中的项在内存中不是彼此相邻的(链表,我正在关注你呢),你将得不到免费缓存加载所带来的优势。并且在这些数据结构中的每一个项都可能会出现缓存未命中。

2.4 缓存行带来的问题

不过,所有这种免费加载有一个弊端。设想你有两个单独并且是相连的long类型变量,我们称它为head,另一个称它为tail。现在,当你加载head到缓存的时候,很可能你也免费加载了tail。这样就会带来一个问题,当core1修改了head变量时,core2需要读取tail变量时,由于缓存一致性协议,虽然core1对tail变量没有任何修改,缓存行和主存中对应对内容都更新了,core2对应的缓存行已经失效,需要重新读取。
请记住我们必须以整个缓存行作为单位来处理(这是CPU的实现所规定的),不能只把head标记为无效。如果两个线程同时分别写head和tail变量,那么对性能对影响会更糟。
在这里插入图片描述
当然解决这个问题对方法,就是将变量对长度增加为缓存行对长度。

3. volatile原理

如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存,由于缓存一致性协议的影响,每个处理器都会检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
总结起来就有以下两点:

  1. Lock前缀指令会引起处理器缓存回写到内存
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

4. volatile使用优化

为了不使多个变量同时在一个缓存行中,可以将变量的字节数转换为与缓存行的字节数相等,这样一个缓存行就只能存储一个变量,避免了伪共享对性能对影响,但是如果变量不会被频繁的读写,其实引起多个变量在同一个缓存行中冲突的概率会比较小,这样做的意义也不是很大。

部分内容参考:https://www.jianshu.com/p/e338b550850f

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章