关于volatile的那些事

  • Post author:
  • Post category:其他




一:写在前面的话:



本篇文章大的方面来说会有两个:基础知识铺垫以及重点着墨点闲言碎语就到这里了,下面进入正题在进入本片文章的重点挥发性之前先让我们来了解Java的内存模型。




二:Java的内存模型




1,JAVA内存模型说明了某个线程的内存操作在哪些情况下对其他线程是可见的,从抽象的角度看,JMM定义了线程和主内存之间的抽象关系,线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以供读写共享变量的副本。本地内存是JMM抽象的一个概念,并不是真实存在的。它涵盖了缓存,写缓存区,寄存器以及其他的硬件和编译器优化的.java内存模型的抽象示意如下:




Java的内存分为本地内存和主内存:



主内存存储全局变量:所有实例域,静态域和数组元素都存储在堆内存中,堆内存在线程之间共享,存在内存可见性问题



本地内存存储局部变量:方法定义参数和异常处理器参数不会在线程之间共享,它们没有内存可见性问题,也不受内存模型的影响。



注:可见性是指线程访问共享变量是否为最新值。




2,下面让我们一起来看看和JVM内存模型息息相关的几个概念



2.1,指令重排序



在执行程序时,为了提高性能,编译器和处理器通常会对指令做重排序重排序分为三种类型:



一,编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。



B,指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。



C,内存系统的重排序:处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。




2.2,数据依赖性



如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。




2.3,AS-如果串行语义



不管怎么排序,单线程程序的执行结果不能被改变。编译器,运行时,处理器都必须遵守如-如果串行语义。为了遵守作为-如果串行语义使单线程程序程序员无需担心重排序会干扰他们,也无需担心内存可见性的问题。因为处理器和编译器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。




2.5,之前发生



在JMM中,如果一个操作的执行结果对另一个操作可见,那么这两个操作之间必须要存在的之前发生关系常见的与开发人员密切相关的之前发生如下:



一,程序顺序规则:一个线程中的每个操作,之前发生于该线程中的任意后续操作。



B,监视器锁规则:对一个锁的解锁,之前发生于随后对这个锁的加锁。



C,易挥发变量规则:对一个挥发性变量的写,之前发生于任意后续对这个挥发性变量的读



d,传递性:如果A发生在B之前,且B发生在C之前,那么A发生在C之前。




下面对上述四个规则做如下解释:



2.5.1,程序顺序规则:举例说明





这段代码看起来是有序执行的,A-> B-> C,也就是这条规则所说的书写在前面的操作先行发生在书写后面的操作。但是JMM可能会对程序代码进行指令重排序,B-> A-> C,虽然进行了重排序,但是执行的结果与程序顺序执行的结果一致因为ç是依赖于阿和B,JMM不会发生下面的重排序:C->乙 – > A,C-> AB,因为这样程序的结果将会发生改变这个列子也很好的说明了数据依赖性和AS-如果串行语义。




2.5.2,监视器锁规则,传递性二者放在一起举例说明代码如下:




根据之前发生规则,这个程序建立的之前发生关系可以分为如下几类:



1,根据程序次序规则:1发生在2,2发生之前-3,4发生之前-5,5发生之前-6之前



2,根据监视器锁规则:3发生在4之前



3,根据传递性:2发生在5,1发生之前-6之前



图形化显示如下:




上述列子表明线程乙获取锁必须要在线程甲释放同一个锁之后发生。也就是说同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能进行加锁的​​动作。




2.5.3,挥发性变量规则:示列代码如下:




根据之前发生规则,这个程序建立的之前发生关系可以分为如下几类:



1,根据程序次序规则:1发生在2,3发生之前-4之前



2,根据volatile变量规则:2发生在3之前



3,根据传递性:1发生在4之前



图形化显示如下:





上述示列说明:





A线程先去写一个volatile变量,然后B线程去进行读取该volatile变量,那么写入操作肯定会先行发生于读操作。




注:两个操作之间具有happen-before关系,并不意味着前一个操作必须要在后一个操作之前执行.happens-before仅仅要求前一个操作的执行结果对后一个操作可见并且前一个操作按顺序排在第二个操作之前。




2.6,顺序一致性内存模型



顺序一致性内存模型有两大特性:



一,一个线程中的所有操作必须按照程序的顺序来执行



B,不管程序是否同步,所有线程都只能看到一个单一的操作执行顺序,在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见JMM对在正确同步的多线程程序,该程序的执行结果与在顺序一致性模型中的执行结果相同。



不同点是:在JMM中,临界区内的代码可以做重排序,这种重排序没有改变程序的执行结果,反而提高了执行的效率。




三:深入理解挥发性关键字



1,





在多线程并发编程中同步一直是元老级角色,很多人都会称呼它为重量级锁,易失性是轻量级的同步,它在并发场景下保证了共享变量的可见性










2,





如果对声明了挥发性的变量进行写操作,JVM就会向处理器发送一条锁定前缀的指令,将这个变量所在缓存行的数据写回到系统内存


。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,执行再计算操






动词}就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的


,实现就会一缓存






致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了


,当






处理器发现自己缓存行对应的内存地址被修改,将就会当前处理器的缓存行设置分类中翻译无效状






态,当处理器对这个数据进行修改操作的时候


,会重新从系统内存中把数据读到处理器缓存里










3,锁定前缀指令在多核处理器下会发生下面两件事情:



一,将当期处理器缓存行的数据写回到系统内存



B,这个写回操作会使在其他CPU里缓存了该内存地址的数据无效





4,易失性变量自身具有如下的特性:





一个,对一个易失性变量的读,总能看到任意线程对这个挥发性变量最后的写入。





B,对任意单个挥发性变量的读写具有原子性,但类似于易失性++这种复合操作不具有原子性。




这里着重解释下b点,很多人会认为挥发性变量的所有操作都是具有原子性的,PS:我当时也是这么认为的,示列代码如下:



执行结果:




这里的结果为:13148,如果我++的操作是线程安全的,那么预期的结果应该为20000,说明挥发++存在并发问题,易失性++,有一次读一次写,任意的读操作在JSR-133内存模型中都必须具有原子性。这也说明了挥发性不具有锁的特性。




5,易失性的内存语义



5.1:当写一个易失性变量时,JMM会把该线程对应的本地内存的共享变量值刷新到主内存中



下面来让我们看个例子:




假设线程甲首先执行写入方法,线程乙随后执行方法读,初始两个线程的本地内存中的标志和一个都是初始状态,下图是线程甲执行挥发性变量写后的共享变量状态示意图:





线程甲在写标志变量后,本地内存甲被线程甲更新过的两个共享变量的值被刷新到主存中,此时本地内存甲和主内存中的共享变量值是一致的。




5.2 volatile读的内存语义



当读一个易失性变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内从中读取共享变量。



下图是线程乙读取一个易失性变量后,共享变量的状态示意图:




5.3 volatile写和读内存语义总结:



一个。线程甲写一个易失性变量,实质上是线程甲向接下来将要读这个挥发性变量的某个线程发出了(其对共享变量所做修改的)消息。




·B·线程乙读一个易失性变量,实质上是线程乙接收了之前某个线程发出的(在写这个挥发性变量之前对共享变量所做修改的)消息。




C·线程甲写一个易失性变量,随后线程乙读这个挥发性变量,这个过程实质上是线程阿通过主内存向线程乙发送消息。




6,易失性的应用场景



6.1,加锁机制既可以确保可见性又可以确保原子性,而挥发性变量只能确保可见性,当且仅当满足以下所有条件时,才应该使用挥发性变量:



一个,对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量的值



B,该变量不会与其他状态变量一起纳入不变性条件中



C,在访问变量时不需要加锁




6.2,下面列举几个开发中常用的应用场景



6.2.1,单例模式:一个基于双重检查的懒加载的单例模式






我们先来看看instance = new Singleton()的语义:





1.分配对象的内存空间





2.初始化对象





3.将实例对象指向刚分配的内存空间,对象的初始化全部完成







如果没有加





volatile修饰instance会发生什么?









假设上述代码中没有用





volatile修饰实例,





如果实例为空,则加锁,只有一个线程进入同步块完成对象的初始化,然后实例不为空,那么后续的所有线程获取实例都不用加锁,从而提升了性能。但是对象赋值的操作步骤可能会存在重排序


,即当前线程的步骤4执行到一半,其它线程如果进来执行到步骤1,实例已经不为空,因此将会读取到一个没有初始化完成的对象。但如果将例如用挥发性来修饰,就完全不一样了,对实例的写入操作将会变成一个原子操作,没有初始化完,就不会被刷新到主存中。





6.2.2:状态标记量:用于终止线程









更多的应用可参考:





Java并发实践





参考文章:





https://www.ibm.com/developerworks/cn/java/j-jtp06197.html





https://www.cnblogs.com/tangyanbo/p/6538488.html





http://blog.csdn.net/suifeng3051/article/details/52611310





https://www.jianshu.com/p/7798161d7472




java并发编程的艺术,Java Concurrency in Practice


















CSDN文章同步会慢些,欢迎关注微信公众号










版权声明:本文为rui920418原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。