Disruptor为什么会如此快(三)揭秘内存屏障(volatile)
前言
本文讨论什么是内存屏障(其实它是软件协调硬件工作的一个指令),以及如何应用volatile(在JAVA世界中内存屏障的语义是由volatile关键字来实现的),最后介绍在disruptor中如何应用的。
什么是内存屏障
它是一个CPU指令。我们在讨论CPU级别的东西,以便获得我们想要的性能(Martin著名的Mechanical Sympathy理论https://mechanical-sympathy.blogspot.com/2011/07/memory-barriersfences.html)。
基本上,它是这样一条指令: a)确保一些特定操作执行的顺序; b)影响一些数据的可见性(可能是某些指令执行后的结果)。
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。
内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
和JAVA什么关系
JAVA里有个关键字叫volatile, 在JAVA世界中内存屏障的语义是由volatile关键字来实现的 。JAVA 语言是支持多线程的,为了解决多线程的安全问题 JAVA 语言引进了 synchronized 同步块和 volatile关键字机制。
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的"可见性"。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
为什么要使用volatile
解决多线程下数据安全共享的问题;volatile变量修饰符如果使用恰当的话,它比synchronized的使用和 执行成本会更低 ,因为它不会引起线程上下文的切换和调度。
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前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
小贴士:
这两件事情在IA-32软件开发者架构手册的第三册的多处理器管理章节(第八章)中有详细阐述。
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使用误区
volatile很容易被误用,用来进行原子性操作。
与使用 synchronized相比,声明一个 volatile 字段的区别在于没有涉及到锁操作。但特别的是对 volatile 字段进行" ++ "这样的读写操作不会被当做原子操作执行。同时测试了字符串拼接的操作,也是不安全的。下文中有代码已经证实 volatile 关键字修饰的变量在多线程写的情况下不安全。
public class Counter { // public volatile static int count = 0; public volatilestatic StringBuilder count = new StringBuilder("aa"); public static void inc(String a){ //这里延迟1毫秒,使得结果明显 try { Thread.sleep(1); } catch (InterruptedExceptione) { } count .append(a+"-"); } public static void main(String[]args) { //同时启动1000个线程,去进行i++计算,看看实际结果 for (int i = 1; i <= 1000; i++) { int finalI = i; new Thread(new Runnable(){ @Override publicvoid run() { Counter.inc(String.valueOf(finalI)); } }).start(); } //这里每次运行的值都有可能不同,可能为1000 System.out.println("运行结果:Counter.count=" + Counter.count); } }
运行结果还是没有我们期望的 aa-XX-1000,下面我们分析一下原因。
在 java 垃圾回收整理一文中,描述了 jvm 运行时刻内存的分配。其中有一个内存区域是 jvm 虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值 load 到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,变量的具体值 load 到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图描述这些交互。
read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容, 其中 use and assign 可以多次出现,但是这一些操作并不是原子性,也就是在 read load 之后,如果主内存 count 变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。
对于 volatile 修饰的变量, jvm 虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
例如假如线程 1 ,线程 2 在进行 read,load 操作中,发现主内存中 count 的值都是 5 ,那么都会加载这个最新的值
在线程 1 堆 count 进行修改之后,会 write 到主内存中,主内存中的 count 变量就会变为 6
线程 2 由于已经进行 read,load 操作,在进行运算之后,也会更新主内存 count的变量值为 6
导致两个线程及时用 volatile 关键字修改之后,还是会存在并发的情况。
暂且不管在多线程下对 volatile 修饰的变量进行修改是否安全的,但是在多线程下对 volatile 修饰的变量进行读操作是安全的,后一点是无可非议的。所以有了以下的结论: volatile 的适用场景当只有一个线程可以修改字段的值,其它线程可以随时读取,那么把字段声明为 volatile 是合理的。
对性能的影响
内存屏障作为另一个CPU级的指令,没有锁那样大的开销,可以看之前文章
Disruptor为什么会如此快 - (一)锁的成本的实验结果。内核并没有在多个线程间干涉和调度。
Disruptor如何使用内存屏障
下图是Disruptor的AbsttactSequencer源码截图。
把Sequence定义为volatile 类型,供多线程共享。从截图可以看出gatingSequences在对象实例化时进行了初始化(写操作),多处进行读操作。这个与先前得出的结论颇为相似: volatile 的适用场景当只有一个线程可以修改字段的值,其它线程可以随时读取,那么把字段声明为 volatile 是合理的。
小结
1、内存屏障是CPU指令,它允许你对数据什么时候对其他进程可见作出假设。在Java世界里,使用volatile关键字来实现内存屏障。使用volatile意味着不用被迫选择加锁,并且还能获得性能的提升。
2、volatile的适用场景:当只有一个线程可以修改字段的值,其它线程可以随时读取,那么把字段声明为 volatile 是合理的。
如果此文能帮小伙伴答疑解惑,请 关注「架构那些事儿」
你的关注就是我的动力!
心级服务智能马桶,关爱您的健康几年前,如果提到智能马桶这个词,可能很多人都觉得这是一个稀罕物。但是随着近几年数字化进程加快,根据相关调查数据显示,智能马桶越来越普及,用户对于智能化马桶的接受度越来越高。中国移动
自播时代,企业不懂直播怎么办?11月11日,用10余年的时间完成了从普通的日期到光棍节,再到双11的角色变换。无数网友早已习惯在这个特定的时间里守在电脑或手机屏幕前,做最后的购物冲刺。双11,经历时间的洗礼,成
心级服务合作伙伴好帮手串码管理工具和家亲生态合作平台是合作伙伴智能设备入库一站式服务平台以及能力开放的对外集成展示平台,平台提供三大服务中心接入服务中心,能力服务中心,方案服务中心,合作伙伴可以在平台上尽情找合作找
心级服务广大普通影迷的家庭影院时代真的到来了在刚过去的2021国庆假期,长津湖我和我的父辈等大片扎堆上映。随着经济的快速发展,人们对精神文化的需求日益提升,中国电影市场迎来蓬勃发展的新时期但同时也面临着如何持续供给高质量内容
心级服务全屋智能是一种怎样的体验?当疲惫一天回到家,喜欢的灯光香氛和电视内容自动开启,耳边响起舒心的音乐,厨房的饭菜已自动烧好,那些曾经只有在电影中出现的场景,正在慢慢走进我们的家。日前,IDC发布中国智能家居设备
心级服务不止能上网,WiFi还能智能感知?日常生活中你是否有遇到过这样的烦恼?外地工作,家里老人独居生活不放心?出门在外,担心室内防盗不安全?家中智能摄像头,屡屡被破解,网上公然贩卖,隐私问题何时休?红外传感器精度低误报频
心级服务和家亲WiFi助手服务,打造智能家庭网络随着居民收入提高生活条件改善科学技术发展,人们的娱乐生活日益丰富,对于家庭网络的智能体验要求也越来越高。智能化的家庭网络不仅为用户提供高质量的上网服务,保障日常的上网需求,同时也可
心级服务双十一抢购,网速不行怎么办?又到一年双十一,你是否已经沉浸在各大主播的直播间无法自拔?购物车是否已经加满了预购连接,就等一声令下?是否想到去年因为网速不够,而痛失的心动好物?你是否在纠结选择哪家宽带服务?!家
心级服务快节奏的生活下,如何保护我们的健康?现在的生活节奏太快,年轻人活得就像是装满燃料的发动机,一直在不停地运转,但是再好的发动机如果你不爱护她不保养她,也会有闹情绪甚至罢工的时候。真到了这种时候,你是不是又会拍着大腿仰天
心级服务快节奏的生活下,如何保护我们的健康?现在的生活节奏太快,年轻人活得就像是装满燃料的发动机,一直在不停地运转,但是再好的发动机,如果你不爱护她不保养她,也会有闹情绪甚至罢工的时候。真到了这种时候,你是不是又会拍着大腿仰
管住嘴,迈开腿!Yolanda智能营养秤帮你监测每日食物热量摄入最近比较忙,出去运动的时间也很少。没有运动的时间,就只能依靠控制饮食来达到控制体重的目的。从上个月开始,减少了碳水类的摄入,减少了米饭的摄入,改为多吃一些沙拉豆腐。结果每天都能稳定