volatile关键字在并发中有哪些作用?
由于计算机为了充分利用CPU的高性能,以及各个硬件存取速度巨大的差异带来的一系列问题为了充分压榨CPU的性能,CPU会对指令乱序执行或者语言的编译器会指令重排,让CPU一直工作不停歇,但同时会导致有序性问题。为了平衡CPU的寄存器和内存的速度差异,计算机的CPU增加了高速缓存,但同时导致了可见性问题为了平衡CPU与IO设备的速度差异,操作系统增加了进程、线程概念,以分时复用CPU,但同时导致了原子性问题。
Java是最早尝试提供内存模型的编程语言。由于Java语言是跨平台的,另外各个操作系统总存在一些差异,Java在物理机器的基础上抽象出一个内存模型(JMM),来简化和管理并发程序。我们都知道Java并发的三大特性:原子性,可见性,有序性原子性指的是一个不可以被分割的操作,即这个操作在执行过程中不能被中断,要么全部不执行,要么全部执行。且一旦开始执行,不会被其他线程打断。可见性指的是一个线程修改了共享变量后,其他线程能立即感知这个变量被修改。有序性指程序按照代码的先后顺序执行。在Java内存模型中,为了提升效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响
那么本文我们就聊聊关键字volatile,可能是Java中最微妙和最难用的关键字,看看其在Java内存模型中是如何保证并发操作的原子性、可见性、有序性的?什么是volatile关键字
volatile是Java中用于修饰变量的关键字,其可以保证该变量的可见性以及顺序性,但是无法保证原子性。更准确地说是volatile关键字只能保证单操作的原子性,比如x1,但是无法保证复合操作的原子性,比如x
其为Java提供了一种轻量级的同步机制:保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。相比于synchronized关键字(synchronized通常称为重量级锁),volatile更轻量级,开销低,因为它不会引起线程上下文的切换和调度。保证可见性
可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。我们一起来看一个例子:publicclassVisibilityTest{privatebooleanflagtrue;publicvoidchange(){flagfalse;System。out。println(Thread。currentThread()。getName(),已修改flagfalse);}publicvoidload(){System。out。println(Thread。currentThread()。getName(),开始执行。。。。。);inti0;while(flag){i;}System。out。println(Thread。currentThread()。getName(),结束循环);}publicstaticvoidmain(String〔〕args)throwsInterruptedException{VisibilityTesttestnewVisibilityTest();线程threadA模拟数据加载场景ThreadthreadAnewThread(()test。load(),threadA);threadA。start();让threadA执行一会儿Thread。sleep(1000);线程threadB修改共享变量flagThreadthreadBnewThread(()test。change(),threadB);threadB。start();}}
其中:threadA负责循环,threadB负责修改共享变量flag,如果flagfalse时,threadA会结束循环,但是上面的例子会死循环!原因是threadA无法立即读取到共享变量flag修改后的值。我们只需privatevolatilebooleanflagtrue;,加上volatile关键字threadA就可以立即退出循环了。
其中Java中的volatile关键字提供了一个功能:那就是被volatile修饰的变量P被修改后,JMM会把该线程本地内存中的这个变量P,立即强制刷新到主内存中去,导致其他线程中的volatile变量P缓存无效,也就是说其他线程使用volatile变量P在时,都是从主内存刷新的最新数据。而普通变量的值在线程间传递的时候一般是通过主内存以共享内存的方式实现的;
因此,可以使用volatile来保证多线程操作时变量的可见性。除了volatile,Java中的synchronized和final两个关键字以及各种Lock也可以实现可见性。加锁的话,当一个线程进入synchronized代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。保证有序性
有序性,顾名思义即程序执行的顺序按照代码的先后顺序执行。但现代的计算机中CPU中为了能够让指令的执行尽可能地同时运行起来,提示计算机性能,采用了指令流水线。一个CPU指令的执行过程可以分成4个阶段:取指、译码、执行、写回。这4个阶段分别由4个独立物理执行单元来完成。
理想的情况是:指令之间无依赖,可以使流水线的并行度最大化但是如果两条指令的前后存在依赖关系,比如数据依赖,控制依赖等,此时后一条语句就必需等到前一条指令完成后,才能开始。所以CPU为了提高流水线的运行效率,对无依赖的前后指令做适当的乱序和调度,即现代的计算机中CPU是乱序执行指令的
另一方面,只要不会改变程序的运行结果,Java编译器是可以通过指令重排来优化性能。然而,重排可能会影响本地处理器缓存与主内存交互的方式,可能导致在多线程的情况下发生细微的BUG。
指令重排一般可以分为如下三种类型:编译器优化重排序,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。指令级并行重排序,现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。内存系统重排序,由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。这并不是显式的将指令进行重排序,只是因为缓存的原因,让指令的执行看起来像乱序。
从Java源代码到最终执行的指令序列,一般会经历下面三种重排序:
变量初始化赋值
我们一起来看一个例子,让大家体悟volatile关键字的禁止指令重排的作用:inti0;intj0;intk0;i10;j1;
对于上面的代码我们正常的执行流程是:
初始化i初始化j初始化ki赋值j赋值
但由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。语句可能的执行顺序如下:
初始化ii赋值初始化jj赋值初始化k
指令重排对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序,提升性能。指令重排不会影响单线程的执行结果,但是会影响多线程并发执行的结果正确性。但当我们用volatile修饰变量k时:inti0;intj0;volatileintk0;i10;j1;
这样会保证上面代码执行顺序:变量i和j的初始化,在volatileintk0之前,变量i和j的赋值操作在volatileintk0后面懒汉式单例双重校验锁volatile版
我们可以使用volatile关键字去阻止重排volatile变量周围的读写指令,这种操作通常称为memorybarrier(内存屏障),详情可见:mp。weixin。qq。comsTyiCfVMee中懒汉式单例双重校验锁volatile版隐藏特性
volatile关键字除了禁止指令重排的作用,还有一个特性:当线程向一个volatile变量写入时,在线程写入之前的其他所有变量(包括非volatile变量)也会刷新到主内存。当线程读取一个volatile变量时,它也会读取其他所有变量(包括非volatile变量)与volatile变量一起刷新到主内存。尽管这是一个重要的特性,但是我们不应该过于依赖这个特性,来自动使周围的变量变得volatile,若是我们想让一个变量是volatile的,我们编写程序的时候需要非常明确地用volatile关键字来修饰。无法保证原子性
volatile关键字无法保证原子性,更准确地说是volatile关键字只能保证单操作的原子性,比如x1,但是无法保证复合操作的原子性,比如x
所谓原子性:即一个或者多个操作作为一个整体,要么全部执行,要么都不执行,并且操作在执行过程中不会被线程调度机制打断;而且这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换(contextswitch)int0;语句1,单操作,原子性的操作i;语句2,复合操作,非原子性的操作
其中:语句2i其实在Java中执行过程,可以分为3步:i被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增使用栈顶值更新局部变量表(寄存器更新写入内存)
执行上述3个步骤的时候是可以进行线程切换的,或者说是可以被另其他线程的这3步打断的,因此语句2不是一个原子性操作volatile版i
我们再来看一个例子:publicclassTest1{publicstaticvolatileintval;publicstaticvoidadd(){for(inti0;i1000;i){val;}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{Threadt1newThread(Test1::add);Threadt2newThread(Test1::add);t1。start();t2。start();t1。join();等待该线程终止t2。join();System。out。println(val);}}
2个线程各循环2000次,每次1,如果volatile关键字能够保证原子性,预期的结果是2000,但实际结果却是:1127,而且多次执行的结果都不一样,可以发现volatile关键字无法保证原子性。synchronized版i
我们可以利用synchronized关键字来解决上面的问题:publicclassSynchronizedTest{publicstaticintval;publicsynchronizedstaticvoidadd(){for(inti0;i1000;i){val;}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{Threadt1newThread(SynchronizedTest::add);Threadt2newThread(SynchronizedTest::add);t1。start();t2。start();t1。join();等待该线程终止t2。join();System。out。println(val);}}
运行结果:2000Lock版i
我们还可以通过加锁来解决上述问题:publicclassLockTest{publicstaticintval;staticLocklocknewReentrantLock();publicstaticvoidadd(){for(inti0;i1000;i){lock。lock();上锁try{val;}catch(Exceptione){e。printStackTrace();}finally{lock。unlock();解锁}}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{Threadt1newThread(LockTest::add);Threadt2newThread(LockTest::add);t1。start();t2。start();t1。join();等待该线程终止t2。join();System。out。println(val);}}
运行结果:2000Atomic版i
Java从JDK1。5开始提供了java。util。concurrent。atomic包(以下简称Atomic包),这个包中的原子操作类,靠CAS循环的方式来保证其原子性,是一种用法简单、性能高效、线程安全地更新一个变量的方式。
这些类可以保证多线程环境下,当某个线程在执行atomic的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个线程执行。
我们来用atomic包来解决volatile原子性的问题:publicclassAtomicTest{publicstaticAtomicIntegervalnewAtomicInteger();publicstaticvoidadd(){for(inti0;i1000;i){val。getAndIncrement();}}publicstaticvoidmain(String〔〕args)throwsInterruptedException{Threadt1newThread(AtomicTest::add);Threadt2newThread(AtomicTest::add);t1。start();t2。start();t1。join();等待该线程终止t2。join();System。out。println(val);}}
运行结果:2000,如果我们维护现有的项目,如果遇到volatile变量最好将其替换为Atomic变量,除非你真的特别了解volatile。Atomic就不展开说了,先挖个坑,以后补上volatile原理
当大家仔细读完上文的懒汉式单例双重校验锁volatile版,会发现volatile关键字修饰变量后,我们反汇编后会发现多出了lock前缀指令,lock前缀指令在汇编中LOCK指令前缀功能如下:被修饰的汇编指令成为原子的与被修饰的汇编指令一起提供内存屏障效果(lock指令可不是内存屏障)
内存屏障主要分类:一类是可以强制读取主内存,强制刷新主内存的内存屏障,叫做Load屏障和Store屏障另一类是禁止指令重排序的内存屏障,主要有四个分别叫做LoadLoad屏障、StoreStore屏障、LoadStore屏障、StoreLoad屏障
这4个屏障具体作用:LoadLoad屏障:(指令Load1;LoadLoad;Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。LoadStore屏障:(指令Load1;LoadStore;Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。StoreStore屏障:(指令Store1;StoreStore;Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。StoreLoad屏障:(指令Store1;StoreLoad;Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
对于volatile操作而言,其操作步骤如下:每个volatile写入之前,插入一个StoreStore,写入以后插入一个StoreLoad每个volatile读取之前,插入一个LoadLoad,读取之后插入一个LoadStore
我们再总结以下,用volatile关键字修饰变量后,主要发生的变化有哪些?:当一个线程修改了volatile修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。即volatile关键字保证了并发的可见性
使用volatile关键字修饰共享变量后,每个线程要操作该变量时会从主内存中将变量拷贝到本地内存作为副本,但当线程操作完变量副本,会强制将修改的值立即写入主内存中。然后通过CPU总线嗅探机制告知其他线程中该变量副本全部失效,(在CPU层,一个处理器的缓存回写到内存会导致其他处理器的缓存行无效),若其他线程需要该变量,必须重新从主内存中读取。在x86的架构中,volatile关键字底层含有lock前缀的指令,与被修饰的汇编指令一起提供内存屏障效果,禁止了指令重排序,保证了并发的有序性
确保一些特定操作执行的顺序,让cpu必须按照顺序执行指令,即当指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;volatile关键字无法保证原子性,更准确地说是volatile关键字只能保证单操作的原子性,比如x1,但是无法保证复合操作的原子性,比如x。有人可能问赋值操作是原子操作,本来就是原子性的,用volatile修饰有什么意义?在Java数据类型足够大的情况下(在Java中long和double类型都是64位),写入变量的过程分两步进行,就会发生Wordtearing(字分裂)情况。JVM被允许将64位数量的读写作为两个单独的32位操作执行,这增加了在读写过程中发生上下文切换的可能性,多线程的情况下可能会出现值会被破坏的情况
在缺乏任何其他保护的情况下,用volatile修饰符定义一个long或double变量,可阻止字分裂情况
爷爷前两天,88岁的爷爷没能抗过这次疫情,走完了自己孤独又惨淡的一生一早爸爸打电话来告诉我爷爷没了,让我看看能不能给他找到顺风车将他送到三百公里外的爷爷家,听后心中冰凉,我已好些年没去
民政大小事一分钟知晓(2022年12月22日)大家好!今天是2022年12月22日,农历十一月廿九,星期四,这些与民政相关的资讯,值得关注!要闻12月20日,国务院总理李克强主持召开国务院常务会议,提出做好困难群众生活保障,该
学习贯彻党的二十大精神,云浮这场培训班直达基层一线12月22日,云浮市举办2022年全市基层党组织书记党务工作者党代表学习贯彻党的二十大精神示范培训班。培训班以视频形式举办,开至县镇村三级,全市6623名各领域基层党组织书记党务工
喜讯!花都5个农产品上榜全国名特优新农产品名录近日,农业农村部农产品质量安全中心公布2022年第三批全国名特优新农产品名录,其中,广州市花都区的5个农产品赫然在列,它们分别是花都蓝莓花都无花果花都西洋菜花都彩虹鲷和花都加州鲈,
惠及超2000个个体工商户,三水新增三个秒批秒办事项为深入推动放管服改革,优化营商环境,进一步优化企业和群众的政务服务体验,近日,佛山市三水区政务数据局与烟草专卖局联合对烟草零售许可证补办歇业恢复营业3个事项业务实现秒批秒办。群众通
非人哉铁扇公主私会大士,牛魔王发现真相,老婆爱的不是自己!非人哉中有位女性角色在初登场的时候就非常惊艳,那就是红孩儿的母亲牛魔王的老婆孙悟空的嫂子铁扇公主,这位公主不光人长得好看气质高贵,还在御姐范中透露着一股可爱劲,最重要的是人家还有霸
关于蜂蜜与熊的相遇成为母亲以后,我经常想一个问题,大人和小孩之间,抛开爱责任义务,有没有可能存在真正的友谊?虽然身为母亲,但我并不奢望成为孩子最好的朋友扪心自问,我也不想和我妈做朋友,母女之间总会有
2022年11月河北省对一带一路沿线国家进出口增长27。6冀时客户端报道(河北台苏杭通讯员刘宏灵周俊伟)石家庄海关统计数据显示今年11月,河北对一带一路沿线国家(以下简称沿线国家)进出口205。6亿元人民币(下同),同比(下同)增长27。
入选工信部工业互联平台创新案例!深圳这家智慧停车龙头企业如何做到的?近日,国家工业和信息化部发布2022年工业互联网平台创新领航应用案例名单,全国共137家入选,其中深圳市6家,3家为龙华区内企业。由捷顺科技打造的基于工业互联网平台的智慧停车及服务
微视频生态中国四季之美东风送暖山川河流初解冻蛰虫始振闻风欲动鱼儿欢游冰雪消融草木初萌万物复苏和风暖阳莺飞草长杨柳青千花百卉竞相争媚玄鸟归来莺燕啼暖春雷乍响万物生长阵阵蛙鸣草木生灵向阳生雨水滋养藤柳攀高生
生态中国四季之美东风送暖山川河流初解冻蛰虫始振闻风欲动鱼儿欢游冰雪消融草木初萌万物复苏和风暖阳莺飞草长杨柳青千花百卉竞相争媚玄鸟归来莺燕啼暖春雷乍响万物生长阵阵蛙鸣草木生灵向阳生雨水滋养藤柳攀高生