当前位置: 首页 > Linux

大数据神之路-Java高级特性增强(volatile关键字)

时间:2023-04-06 19:36:26 Linux

请点击GitHub原文:https://github.com/wangzhiwub...大数据之路神系列:请点击GitHub原文:https://github.com/wangzhiwub...Java高级特性增强-集合Java高级特性增强-多线程Java高级特性增强-SynchronizedJava高级特性增强-volatileJava高级特性增强-并发集合框架Java高级特性增强-分布式Java高级特性增强-ZookeeperJava进阶特性增强-JVMJava进阶特性增强-NIO公众号全网唯一帮助Java开发者从0转战大数据领域关注,大数据学习路线最新更新,众多小伙伴已经加入了~Java高级特性增强-Volatile这部分网上有很多资源可以参考,这里整理了一部分,感谢前辈们的贡献,每篇文章末尾都有一个参考列表,源码推荐看JDK1.8之后的版本,注意筛选~多线程集合框架NIOJava并发容器*volatile关键字volatile特性volatile可以说是java提供的最轻量级的同步机制虚拟机。但是synchronized不容易被正确理解,而且由于很多程序员在并发编程中遇到线程安全问题,都会使用synchronized。Java内存模型告诉我们,每个线程都会将共享变量从主内存复制到工作内存中,然后执行引擎会根据工作内存中的数据进行操作。线程在工作内存中运行后什么时候写入主存?对普通变量没有规定这个时机,但是对于volatile修饰的变量,给java虚拟机做了特殊的约定。线程对volatile变量的修改会立即被其他线程感知到,即不会出现数据脏读现象,从而保证数据的“可见性”。通俗地说,线程A对一个volatile变量的修改对其他线程是可见的,即该线程每次获取的volatile变量的值都是最新的。volatile的实现原理是在生成汇编代码时,在写入volatile修饰的共享变量时,会多出一些以Lock为前缀的指令。我们觉得这条Lock指令一定有什么神奇之处,那么在多核处理器下,以Lock为前缀的指令会发生什么变化呢?影响主要有两方面:当前处理器缓存行的数据被写回系统内存;这种回写内存的操作,会在其他CPU中使该内存地址缓存的数据失效。为了提高处理速度,处理器不直接与内存通信,而是先将系统内存中的数据读取到内部缓存(L1、L2或其他)中再进行操作,但不知道什么时候可以运算后写入内存。如果写入声明为volatile的变量,JVM会向处理器发送Lock前缀指令,将变量所在缓存行的数据写回系统内存。但是,即使回写到内存中,如果其他处理器缓存的值还是旧的,那么在执行计算操作时就会出现问题。因此,在多处理器下,为了保证各个处理器的缓存是一致的,会实现缓存一致性协议。每个处理器通过嗅探总线上传播的数据来检查其缓存的值是否已过期,当处理器发现自己的cacheline对应的内存地址被修改时,会将当前处理器的cacheline设置为无效状态。当处理器修改这些数据时,它会从系统内存中重新读取数据到处理器缓存中。因此,经过分析,我们可以得出以下结论:以Lock为前缀的指令,会导致处理器缓存被写回内存;一个处理器的缓存会被写回内存,这会导致其他处理器的缓存失效;当处理器发现本地缓存无效时,会从内存中重新读取变量数据,即可以获得最新的当前值。这样,通过volatile变量的这种机制,每个线程都可以获得该变量的最新值。我们如何在我们的项目中使用它?1、在高并发场景下,如何通过一个boolean变量isopen来控制代码是否遵循促销逻辑?公共类ServerHandler{privatevolatileisopen;publicvoidrun(){if(isopen){//isopen=truelogic}else{//otherlogic}}publicvoidsetIsopen(booleanisopen){this.isopen=isopen}}场景不用太纠结在细节中。这里只是举例说明volatile的使用。用户的请求线程执行run方法。如果需要开启促销活动,可以通过后台设置。具体实现可以发送一个请求,调用setIsopen方法,将isopen设置为true,因为isopen是通过volatile修饰的,所以修改后其他线程可以拿到isopen的最新值,执行isopen=true的逻辑用户请求。2.doublechecksingleton模式的一个实现,但是很多人会忽略volatile关键字,因为没有这个关键字,程序也能很好的运行,但是代码的稳定性并不总是100%,可能在某个时刻未来,隐藏的bug会出来。类Singleton{privatevolatilestaticSingleton实例;publicstaticSingletongetInstance(){if(instance==null){syschronized(Singleton.class){if(instance==null){instance=newSingleton();}}}返回实例;但是,在很多单例模式的实现中,我推荐懒加载和优雅的写法InitializationonDemandHolder(IODH)。公共类Singleton{静态类SingletonHolder{静态Singleton实例=newSingleton();}publicstaticSingletongetInstance(){returnSingletonHolder.instance;}}如何保证内存可见性在java虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,共享主内存的数据,我们看看它们之间的区别操作普通变量和volatile变量:1.对于普通变量:读操作会先读取工作内存中的数据,如果工作内存中不存在,则从主内存中复制一份数据到工作内存中;写操作只会修改工作内存中的复制数据。在这种情况下,其他线程无法读取该变量的最新值。2、对于volatile变量,JMM在读操作时会将工作内存中对应的值作废,要求线程从主存中读取数据;在写操作的过程中,JMM会将工作内存中对应的数据刷新到主内存中,这样其他线程就可以读取到变量的最新值。volatile变量的内存可见性是基于内存屏障(MemoryBarrier)实现的。什么是内存屏障?内存屏障,也称为内存栅栏,是一种CPU指令。程序运行时,为了提高执行性能,编译器和处理器会对指令进行重新排序。为了保证在不同的编译器和CPU上得到相同的结果,JMM通过插入特定类型来禁止特定类型的内存屏障。编译器重排序和处理器重排序,插入一个memorybarrier会告诉编译器和CPU:不管什么指令都可以用这个MemoryBarrier指令重排序。一个例子如下:classSingleton{privatevolatilestaticSingletoninstance;私人诠释;私人诠释b;私人诠释b;publicstaticSingletongetInstance(){if(instance==null){syschronized(Singleton.class){if(instance==null){a=1;//1b=2;//2instance=newSingleton();//3c=a+b;//4}}}返回实例;}}1。如果变量instance没有经过volatile修饰,语句1、2、3可以随意重新排序执行,即指令执行过程可能是3214也可能是1324。2.如果变量instance经过volatile修饰,会产生内存屏障插入语句3之前和之后。通过观察volatile变量和普通变量生成的汇编代码,可以发现在操作volatile变量时会多出一个锁前缀指令:Java代码:instance=newSingleton();汇编代码:0x01a3de1d:movb$0x0,0x1104800(%esi);0x01a3de24:**lock**addl$0x0,(%esp);这条锁前缀指令等同于上面的内存屏障,提供以下保证:1.将当前CPU缓存行的数据写回主存;2、这种回写内存的操作,在其他CPU中会使缓存在该内存地址的数据失效。CPU为了提高处理性能,不直接与内存通信,而是将内存中的数据读取到内部缓存(L1,L2)中进行运算,但不确定何时写回内存操作,如果volatile变量被写入。当CPU执行Lock前缀指令时,会将变量所在缓存行中的数据写回内存。但是,仍然存在问题。即使内存中的数据是最新的,其他CPU也会缓存旧值。因此,为了保证每个CPU的缓存一致性,每个CPU通过嗅探总线上传播的数据来检查自己缓存的数据的有效性。当发现cacheline对应的内存地址的数据被修改时,cache会在CPU读取变量时,发现所在的cacheline被设置为无效,会重新从内存中读取数据到缓存中。这也是我们之前讲的原理部分的解释~volatilehappens-before关系volatile变量可以通过缓存一致性协议保证每个线程都能拿到最新的值,即满足数据的“可见性”。继续上一篇分析问题的方式(我一直认为思考问题的方式是属于我的,是最重要的,我也在不断培养这方面的能力),我一直分并发分析的切入点分为两个一个核心,三个性质。两个核心:JMM内存模型(主内存和工作内存)和happens-before;三个属性:原子性、可见性、有序性(三个属性的总结会在以后的文章中和大家一起探讨)。话不多说,我们先来看两个核心之一:volatile的happens-before关系。六个happens-before规则之一是:volatile变量规则:对volatile字段的写入发生在对该volatile字段的任何后续读取之前。下面我们结合具体的代码,我们使用这个规则来推导:publicclassVolatileExample{privateinta=0;privatevolatilebooleanflag=false;publicvoidwriter(){a=1;//1标志=真;//2}publicvoidreader(){if(flag){//3inti=a;//4}}}上面的示例代码对应下图的happens-before关系:lock线程A执行writerfirst方法,然后线程B执行reader方法。图中的每个箭头和两个节点编码了一个发生前的关系。黑色的代表根据程序顺序规则推导,红色的是根据volatile变量的writehappens-before对后面任意一对volatile变量的读取,蓝色的是根据传递规则推导的。这里的2happens-before3也是根据happens-before规则定义的:如果Ahappens-beforeB,则A的执行结果对B可见,A的执行顺序先于B的执行顺序,我们可以知道,操作2执行的结果对操作3是可见的,也就是说当线程A将volatile变量flag变为true时,线程B可以很快感知到。参考文章和书籍:《Java并发编程的艺术》《实战Java高并发程序设计》https://blog.csdn.net/qq_3433...https://www.jianshu.com/p/a5f...https://www.jianshu.com/p/506...Github:关注公众号,推荐,访谈,资源下载,更多大数据技术~预计更新500+篇,已更新50+篇~