精华内容
下载资源
问答
  • java经典面试题之volatile底层实现原理
    2020-04-12 10:48:54

    前言

    当共享变量被声明为volatile后,对这个变量的读/写操作都会很特别,下面我们就揭开volatile的神秘面纱。

    1.volatile的内存语义

    1.1 volatile的特性

    一个volatile变量自身具有以下三个特性:

    1. 可见性:即当一个线程修改了声明为volatile变量的值,新值对于其他要读该变量的线程来说是立即可见的。而普通变量是不能做到这一点的,普通变量的值在线程间传递需要通过主内存来完成。

    2. 有序性:volatile变量的所谓有序性也就是被声明为volatile的变量的临界区代码的执行是有顺序的,即禁止指令重排序。

    3. 受限原子性:这里volatile变量的原子性与synchronized的原子性是不同的,synchronized的原子性是指只要声明为synchronized的方法或代码块儿在执行上就是原子操作的。而volatile是不修饰方法或代码块儿的,它用来修饰变量,对于单个volatile变量的读/写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的。并且在多线程环境中,volatile并不能保证原子性。

    1.2 volatile写-读的内存语义

    volatile写的内存语义:当写线程写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

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

    2.volatile语义实现原理

    在介绍volatile语义实现原理之前,我们先来看两个与CPU相关的专业术语:

    • 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。

    • 缓存行(cache line):CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行。

    2.1 volatile可见性实现原理

    volatile可见性的内存语义是如何实现的呢?下面我们看一段代码,并将代码生成的处理器的汇编指令打印出来(关于如何打印汇编指令,我会在文章末尾附上),看下对volatile变量进行写操作时,CPU会做什么事情:

    public class VolatileTest {
    
        private static volatile VolatileTest instance = null;
    
        private VolatileTest(){}
    
        public static VolatileTest getInstance(){
            if(instance == null){
                instance = new VolatileTest();
            }
    
            return instance;
        }
    
        public static void main(String[] args) {
            VolatileTest.getInstance();
        }
    }

    以上的代码是一个我们非常熟悉的在多线程环境中不能保证线程安全的单例模式代码,这段代码中特殊的地方是,我将实例变量instance加上了volatile修饰,下面看打印的汇编指令:

    image.png

     

    上面截图中,我们看到我划线的一行的末尾有一句汇编注释:putstatic instance,了解JVM 字节码指令的小伙伴都知道,putstatic的含义是给一个静态变量设置值,在上述代码中也就是给静态变量instance赋值,对应代码:instance = new VolatileTest();在getInstance方法中为instance实例化,因为instance加了volatile修饰,所以给静态变量instance设置值也是在写一个volatile变量。

    看到上述有汇编指令,也有字节码指令,大家会不会混淆这两种指令,这里我指明一下字节码指令和汇编指令的区别:

    我们都知道java是一种跨平台的语言,那么java是如何实现这种平台无关性的呢?这就需要我们了解JVM和java的字节码文件。这里我们需要有一点共识,就是任何一门编程语言都需要转换为与平台相关的汇编指令才能够最终被硬件执行,比如C和C++都将我们的源代码直接编译成与CPU相关的汇编指令给CPU执行。 不同系列的CPU的体系架构不同,所以它们的汇编指令也有不同,比如X86架构的CPU对应于X86汇编指令,arm架构的CPU对应于arm汇编指令。如果将程序源代码直接编译成与硬件相关的底层汇编指令,那么程序的跨平台性也就大打折扣,但执行性能相对较高。为了实现平台无关性,java的编译器javac并不是将java的源程序直接编译成与平台相关的汇编指令,而是编译成一种中间语言,即java的class字节码文件。字节码文件,顾名思义存的就是字节码,即一个一个的字节。有打开过java字节码文件研读过的小伙伴可能会发现,字节码文件里面存的并不是二进制,而是十六进制,这是因为二进制太长了,一个字节要由8位二进制组成。所以用十六进制标表示,两个十六进制就可以表示一个字节。java源码编译后的字节码文件是不能够直接被CPU执行的,那么该如何执行呢?答案是JVM,为了让java程序能够在不同的平台上执行,java官方提供了针对于各个平台的java虚拟机,JVM运行于硬件层之上,屏蔽各种平台的差异性。javac编译后的字节码文件统一由JVM来加载,最后再转化成与硬件相关的机器指令被CPU执行。知道了通过JVM来加载字节码文件,那么还有一个问题,就是JVM如何将字节码中的每个字节和我们写的java源代码相关联,也就是JVM如何知道我们写的java源代码对应于class文件中的哪段十六进制,这段十六进制是干什么的,执行了什么功能?并且一大堆的十六进制,我们也看不懂啊。所以这就需要定义一个JVM层面的规范,在JVM层面抽象出一些我们能够认识的指令助记符,这些指令助记符就是java的字节码指令。

    lock指令在多核处理器下会引发下面的事件:

    将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。

    为了提高处理速度,处理器一般不直接和内存通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完成后并不知道处理器何时将缓存数据写回到内存。但如果对加了volatile修饰的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量在缓存行的数据写回到系统内存。这时只是写回到系统内存,但其他处理器的缓存行中的数据还是旧的,要使其他处理器缓存行的数据也是新写回的系统内存的数据,就需要实现缓存一致性协议。即在一个处理器将自己缓存行的数据写回到系统内存后,其他的每个处理器就会通过嗅探在总线上传播的数据来检查自己缓存的数据是否已过期,当处理器发现自己缓存行对应的内存地址的数据被修改后,就会将自己缓存行缓存的数据设置为无效,当处理器要对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到自己的缓存行,重新缓存。

    总结下:volatile可见性的实现就是借助了CPU的lock指令,通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:

    写volatile时处理器会将缓存写回到主内存。

    一个处理器的缓存写回到内存会导致其他处理器的缓存失效。

     

    2.2 volatile有序性的实现原理

    volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。

    那么禁止指令重排序又是如何实现的呢?答案是加内存屏障。JMM为volatile加内存屏障有以下4种情况:

    1. 在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
    2. 在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
    3. 在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
    4. 在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。

    上述内存屏障的插入策略是非常保守的,比如一个volatile的写操作后面需要加上StoreStore和StoreLoad屏障,但这个写volatile后面可能并没有读操作,因此理论上只加上StoreStore屏障就可以,的确,有的处理器就是这么做的。但JMM这种保守的内存屏障插入策略能够保证在任意的处理器平台,volatile变量都是有序的。

    3.JSR-133对volatile内存语义的增强

    在JSR-133之前的旧的java内存模型中,虽然不允许对volatile变量之间的操作进行重排序,但允许对volatile变量与普通变量之间进行重排序。比如内存屏障前面是一个写volatile变量的操作,内存屏障后面的操作是一个写普通变量的操作,即使这两个写操作可能会破坏volatile内存语义,但JMM是允许这两个操作进行重排序的。

    在JSR-133以及后面的新的java内存模型中,增强了volatile的内存语义。只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义,这种重排序就会被编译器重排序规则和处理器内存屏障出入策略禁止。

     

    附:配置idea打印汇编指令

    工具包下载地址:链接:https://pan.baidu.com/s/11yRnsOHca5EVRfE9gAuVxA
    提取码:gn8z

    将下载的工具包解压,复制到jdk安装目录的jre路径下的bin目录中,如图:

    image.png

    然后配置idea,在 VM options 选项中输入:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*类名.方法名

    JRE选项选择已放入工具包的jre路径。

    下图是我的idea配置:

    image.png

    以上配置好后运行就可以打印汇编指令了。

    更多相关内容
  • volatile底层实现原理

    千次阅读 2021-07-21 14:38:15
    Load2 保证Store1的写操作已刷新到主内存后,Load2及之后的读写操作才会执行 StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果 六、volatile的底层原理 volatile底层实现原理是内存屏障,...

    一、并发编程中的三个特性

    • 原子性: 一个或多个操作为一个整体,要么全部执行要么都不执行,synchronized 可以保证代码块的原子性和共享变量的可见性
    • 可见性: 当多个线程共享同一变量时,若其中一个线程对该共享变量进行了修改,那么该修改对其他线程是立即可见的
    • 有序性: 程序执行的顺序与代码的先后顺序相同

    二、JMM内存模型

    在这里插入图片描述

    2.1 JMM数据原子操作

    在这里插入图片描述
    在这里插入图片描述

    2.2 缓存一致性协议(MESI)

    多个CPU从主内存读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存中的数据,该数据会马上同步回主内存,其它CPU通过总线嗅探机制可以感知数据的变化,从而使自己缓存中的数据失效!

    在这里插入图片描述

    三、指令重排

       在不影响单线程程序执行结果的前提下,计算机为了最大程度发挥机器的性能,会对机器指令进行重排序优化。指令重排不会对单线程程序的结果产生影响,但他可能导致多线程程序出现非预期结果。
    在这里插入图片描述

    重排序会遵循as-if-serial和happens-before规则:

    1. as-if-serial不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变, 编译器,runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作进行重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能会被编译器和处理器进行重排序

    2. happens-before

    • 1.程序顺序原则:在一个线程内必须保证语义串行性,也就是说按照代码顺序执行

    • 2.锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)操作之前,也就是说,如果对一个锁解锁后,再加锁,那么加锁动作必须在解锁动作之后(同一个锁)

    • 3.volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单地说,volatile变量再每次被线程访问时,都强迫从主内存中读取该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,不同时刻,不同的线程总是能够看到该变量的最新值

    • 4.线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见

    • 5.传递性:A先于B,B先于C,那么A必然先于C

    • 6.线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A在线程B的join()方法成功返回后,线程B对共享变量的修改对线程A可见

    • 7.线程中断规则:对线程interrupt()方法的调用先行发生于中断线程的代码检测到中断事件的发生,可通过Thread.interruped()方法检测到线程是否发生中断

    • 8.对象终结规则:一个对象的初始化完成先行发生于该对象的finalize()方法的开始。换句话就是,在对象没有完成初始化之前,是不能调用finalize()方法的

    四、CPU层面的内存屏障

    • 写屏障(Store Memory Barrier)通知处理器将写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存
    • 读屏障(Load Memory Barrier)配合写屏障,使得在写屏障之前的内存更新对于读屏障之后的读操作是可见的

    - 可见性
     &写屏障(Store Memory Barrier)会保证在该屏障之前的,对共享变量的改动,都会同步到主存中
     &读屏障(Load Memory Barrier)会保证在该屏障之后的,对共享变量的读取,读取的都是主存中的最新数据
    - 有序性
     &写屏障(Store Memory Barrier)会确保指令重排时,不会将写屏障之前的代码排在写屏障之后
     &读屏障(Load Memory Barrier)会确保指令重排时,不会将读屏障之后的代码排在读屏障之前

    1.如何保证可见性
    写屏障(sfence)会保证在该屏障之前的,对共享变量的改动,都会同步到主存中

    public void actor2(I_Result r) {
     num = 2;
     ready = true; // ready 是 volatile 赋值带写屏障
     // 写屏障
    }
    

    读屏障(lfence)会保证在该屏障之后的,对共享变量的读取,读取的都是主存中的最新数据

    public void actor1(I_Result r) {
     // 读屏障
     // ready 是 volatile 读取值带读屏障
     if(ready) {
     r.r1 = num + num;
     } else {
     r.r1 = 1;
     }
    }
    

    在这里插入图片描述

    2.如何保证有序性
    写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

    public void actor2(I_Result r) {
     num = 2;
     ready = true; // ready 是 volatile 赋值带写屏障
     // 写屏障
    }
    

    读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

    public void actor1(I_Result r) {
     // 读屏障
     // ready 是 volatile 读取值带读屏障
     if(ready) {
     r.r1 = num + num;
     } else {
     r.r1 = 1;
     }
    }
    

    在这里插入图片描述

    五、JVM层面的内存屏障

    屏障类型指令示例说明
    LoadLoadLoad1;LoadLoad;Load2确保Load1的读取操作在Load2及后续所有读取操作之前执行
    StoreStoreStore1;StoreStore;Store2在Store2及其后的写操作之前,保证Store1的写操作已刷新到主内存
    LoadStoreLoad1;LoadStore;Store2在Store2及其后的写操作之前,保证Load1的读操作已结束
    StoreLoadStore1;StoreLoad;Load2保证Store1的写操作已刷新到主内存后,Load2及之后的读写操作才会执行

    StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果

    六、volatile的底层原理

    volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

    • 在每个volatile写操作的前面插入一个StoreStore屏障
    • 在每个volatile写操作的后面插入一个StoreLoad屏障
    • StoreStore屏障将保障上面所有的普通写操作结果在volatile写之前会被刷新到主内存->普通写操作对其他线程可见
    • StoreLoad屏障的作用是避免volatile写操作与后面可能有的volatile读/写操作重排序
      在这里插入图片描述
    • 在每个volatile读操作的后面插入一个LoadLoad屏障
    • 在每个volatile读操作的后面插入一个LoadStore屏障
    • LoadStore屏障用来禁止编译器把上面的volatile读操作与下面的普通读写操作重排序
      在这里插入图片描述
      总之 volatile只能保证可见性和有序性但不能保证原子性,原子性需要通过Synchronized这样的锁机制实现
      在这里插入图片描述

    七、相关问题

    1.锁机制如何保证共享变量可见性?
    当某一个线程进入synchronized代码块前,线程会获得锁,清空工作内存,从主内存拷贝共享变量的最新值到工作内存成为副本,之后执行代码,将修改后的副本的值刷新回主内存中,最后线程释放锁。而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的

    2.volatile如何保证共享变量可见性?
    &在工作内存中,每次使用volatile变量前必须先从主内存刷新变量的最新值到工作内存
    &在工作内存中,每次修改volatile变量后都必须立刻同步回主内存中,用于保证其他线程可以看到当前线程对volatile变量所做的修改

    展开全文
  • Volatile底层实现原理

    2021-04-12 09:42:31
    在java中,关键字volatile那是必须要掌握的,这在多线程并发中大量被使用。从之前的jdk源码也可以知道,volatile和CAS构成了java语言高...那么volatile是怎么实现这两个特性的呢。 可见性: 在了解可见性性之前..

    在java中,关键字volatile那是必须要掌握的,这在多线程并发中大量被使用。从之前的jdk源码也可以知道,volatile和CAS构成了java语言高并发的基石。我们一般会把volatile称为轻量级的锁,有时我们在使用volatile的时候能够达到更高的并发。那么关键字 volatile的作用是什么?我们在实际中如果需要使用volatile的话,无非是保证有序性和可见性。有序性和可见性在之前的 并发的特性中有介绍。那么volatile是怎么实现这两个特性的呢。

     

    可见性

    在了解可见性性之前,我们得熟悉几个cpu的指令编码,这样能方便我们快速理解这块的实现(图片源自java并发编程的艺术)。

     

    图片

     

    如果我们对一个变量加上volatile关键字,那么在编译成汇编的时候会加上lock前缀,那么这个lock前缀就是关键能完成这个可见性的操作,具体实现如下:

    1)将当前处理器的缓存行数据写回到系统内存

    2)这个写回内存的操作会使其他cpu里面缓存的了该内存地址的变为无效

    这两个和我们之前讲的内存模型里面是一致的,就是实现写回主存,其他内存地址无效,那么其他地址如果要读取数据的话,就必须要从主存中从新拉取数据。

     

    有序性

    指令重排序是java为了提升性能而对指令进行重新排序。指令重排序包括以下几个重排序过程:编译器重排序 --》指令集并行重排序 --》内存系统的重排序。实际上就是从代码到cpu执行的一系列过程,为了优化性能,都进行了相应的指令重排序的操作。但是在单线程的情况下,这些指令的重排序是能够保证执行的结果和实际看到代码的顺序的结果是一样的,但是在多线程的情况下 ,指令重排序就可能导致意外的“惊喜”。

    volatile在实现上是通过限制编译器重排序,指令集重排序实现的。Volatile关键字规定了编译器的重排序规则:(图片来自java并发编程的艺术)

     

    图片

     

    从这个表格上我们可以看出以下几点:

    1) 第二个操作是volatile写时,第一个操作无论是啥都不能重排序这个操作保证写前和写后的不会顺序错乱,写前的不会在写后操作。

    2) 第一个操作是volatile读时,第二个操作无论是啥都不能重排序。这个保证volatile读的顺序,读后的不会到读前面。

    3) 第一个操作是volatile写,第二个操作是volatile读时,不能重排序这个保证volatile写在读之前。

     

    Ok,当我们了解了这几条规则之后,我们JMM是怎么实现这些规则的,通过一种叫做内存屏障的保护进行,这个之前在unsafe类中也有提到loadFence,storeFence等类似内存屏障的操作。JMM在使用内存屏障采用的策略如下:

    1)在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障,用来完成对写的保护。 

    2)在每个volatile读操作的后面插入一个LoadLoad屏障和LoadStore屏障,用来完成对读操作的保护。

     

    下面我们看一个例子:

    1. public class Test{  

    2.     volatile int v1 = 1;   

    3.     public void testVolatile() {   

    4.         int i = v1; // 第一个volatile读    

    5.         v1 = i + 1; // 第一个volatile写    

    6.         //...   

    7.     }  

    8. }  

    那么编译器生成的字节码将会如下:

     图片

    上面是编译器处理后的指令,但是有些cpu产商会把这些屏障在进一步优化,但是这个优化实际上是能够保证先后顺序的。

    虽然现在当我们了解了volatile的实现原理,但是实际上我们如果想要用volatile替换锁的话,还是要慎重考虑,因为多线程在实际生产中真的有可能 会导致很诡异的情况,这种往往要定位很久。但是如果项目对性能要求不是特别高的话,使用锁是一种比较保险且有效的方法实现多线程的同步的。

    想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

                                                                       

    展开全文
  • java基础---volatile底层实现原理详解

    千次阅读 2020-04-01 01:15:07
    想知道volatile实现原理首先得去了下解JMM,我们都知道JVM会为每一个thread开辟一块自己的工作空间,在我们操作变量时是从主内存拿到变量的一个副本,然后对副本进行操作后再刷新到主内存中这么一个总体的流程。...

    大家都知道生产中可以使用volatile达到保证可见性和指令重排的目的。但是对其实现原理并不是很清楚,为了加深学习和理解感觉很有必要来写篇博客总结一下。

    JMM—java内存模型

    想知道volatile实现原理首先得去了下解JMM,我们都知道JVM会为每一个thread开辟一块自己的工作空间,在我们操作变量时是从主内存拿到变量的一个副本,然后对副本进行操作后再刷新到主内存中这么一个总体的流程。
    在这里插入图片描述
    先简单来看一下如果要改变一个变量值需要经过哪些操作:
    1.首先会执行一个read操作将主内存中的值读取出来
    2.执行load操作将值副本写入到工作内存中
    3.当前线程执行user操作将工作内存中的值拿出在经过执行引擎运算
    4.将运算后的值进行assign操作写会到工作内存。
    5.线程将当前工作内存中新的值存储回主内存,注意只是此处还没有写入到主内存的共享变量,主内存中的值还没有改变。
    6.最后一步执行write操作将主内存中的值刷新到共享变量,到此处一个简单的流程就走完了。

    下图的8种操作是定义在java内存模型当中的,我们的任何操作都需要通过这几种方式来进行。
    在这里插入图片描述
    简单看了一下操作流程后继续回到volatile关键字,在多个个线程工作内存看起来互无关联的情况下是怎么做到保证变量的可见性的?

    这里我们不得不先去了解一个名词:总线 ------什么是总线?它是干什么的?

    度娘给出的解释: 由于总线是连接各个部件的一组信号线。通过信号线上的信号表示信息,通过约定不同信号的先后次序即可约定操作如何实现。简单来说就是我们的cpu和内存进行交互就得通过总线,它们不能隔空产生连接。总线就是一条共享的通信链路,它用一套线路来连接多个子系统。

    总线按功能和规范可分为五大类型:

    • 数据总线(Data Bus):在CPU与RAM之间来回传送需要处理或是需要储存的数据。
    • 地址总线(Address Bus):用来指定在RAM(Random Access Memory)之中储存的数据的地址。
    • 控制总线(Control Bus):将微处理器控制单元(Control Unit)的信号,传送到周边设备。
    • 扩展总线(Expansion Bus):外部设备和计算机主机进行数据通信的总线,例如ISA总线,PCI总线。
    • 局部总线(Local Bus):取代更高速数据传输的扩展总线。

    最初实现就是通过总线加锁的方式也就是上面的lock与unlock操作,但是这种方式存在很大的弊端。会将我们的并行转换为串行,从而失去了多线程的意义。这里不详细展开了解一下即可。下面才是我们真正需要认识的

    MESI缓存一致性协议:

    CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。为了解决这个问题CPU厂商采用了缓存的解决方案,知道目前我们正在使用的多级的缓存结构。我们可以到任务管理器看一下:
    在这里插入图片描述
    目前流行的多级缓存结构:
    在这里插入图片描述
    多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。这里我们大概只需要有这么一个概念就可以。而当我们共享变量用volatile修饰后就会帮我们在总线开启一个MESI缓存协议。同时会开启CPU总线嗅探机制(监听),当其中一个线程将修改后的值传输到总线时,就会触发总线的监控告诉其他正在使用当前共享变量的线程使当前工作内存中的值失效。这个时候工作空间中的副本变量就没有了,从而需要重新去获取新的值。

    底层实现主要是通过汇编Lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并写回到主内存。总的来说就是Lock指令会将当前处理器缓存行数据立即写回到系统内存从而保证多线程数据缓存的时效性。这个写回内存的操作同时会引起在其它CPU里缓存了该内存地址的数据失效(MESI协议)。

    为了保证在从工作内存刷新回主内存这个阶段主内存数据的安全性,在store前会使用内存模型当中的lock操作来锁定当前主内存中的共享变量。当主内存变量在write操作后才会将当前lock释放掉,别的线程才能继续进来获取新的值。

    查看Java的汇编指令: 想要实际去看下底层的汇编指令,需要在jre/bin目录下添加额外的两个包
    下载链接:百度网盘链接,提取码:d753

    在这里插入图片描述
    然后配置idea,在 VM options 选项中输入:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*类名.方法名或者-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly就可以在控制台看到输出的lock汇编指令。
    在这里插入图片描述
    我们都知道volatile并不能保证原子性,到这里也可以解释通为什么了。假设当前有两个线程同时对共享变量进行+1运算,Thread1比Thread2先进行了Lock操作拿到了锁,此时由于我们的总线嗅探机制Thread2就会知道共享变量值已经修改过了,从而导致当前Thread2工作内存中的副本变量失效。只能再次去主内存中取新的值,但这样无形之中Thread2就已经浪费掉了一次操作机会。从而导致最终结果小于预期的情况出现。(比如最常用到的那种两个线程同时对一个volatile修饰的int进行加减运算的例子)

    提示:
    如 long a = 100L long b = a+1
    在这里a+1并不是我们想象中的原子操作因为long在java中占8个子节一个64位写操作实际上将会被拆分为2个32位的操作,这一行为的直接后果将会导致最终的结果是不确定的并且缺少原子性的保证。
    在Java虚拟机规范中同样也有类似的描述:“For the purposesof the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each32-bit half. This can result in a situation where a thread sees the first 32 bitsof a 64-bit value from one write, and the second 32 bits from anotherwrite.”

    翻译:对于Java编程语言内存模型来说,对非易失性长值或双值的一次写操作被视为两次单独的写操作:一次写32位的一半。这可能导致这样一种情况,一个线程看到一个64位值的前32位从一个写,和第二个32位从另一个写。

    官网地址

    指令重排

    在之前很经典的单例设计模式中为了防止DCL在指令重排后导致线程不安全的情况,就使用了volatile来防止指令重排。

    我们知道为了提高程序执行的性能,编译器和执行器(处理器)通常会对指令做一些优化(重排序)。volatile通过内存屏障实现了防止指令重排的目的。同时lock前缀指令相当于一个内存屏障,它会告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

    不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
    Java内存屏障主要有Load和Store两类:

    • 对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据
    • 对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存
      为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略:
    • 在每个volatile写操作的前面插入一个StoreStore屏障。
    • 在每个volatile写操作的后面插入一个StoreLoad屏障。
    • 在每个volatile读操作的后面插入一个LoadLoad屏障。
    • 在每个volatile读操作的后面插入一个LoadStore屏障。
    展开全文
  • 一.volatile代码package jvm;public classVolatileVisibilityTest {private static boolean initFlag = false;//private static volatile boolean initFlag = false;public static voidmain(String[] args) throws ...
  • Java内存模型及Volatile底层实现原理
  • volatile底层实现原理和其应用

    千次阅读 2018-11-27 06:42:06
    volatile时轻量级的synchronized,它在多处理器开发中保证了数据的读的一致性,意思就是当一个线程修改一个共享变量时,另外一个线程能读到这个共享变量的值。如果volatile变量修饰符使用的恰当的话,他的运行成本会...
  • Synchronize和Volatile底层实现原理

    千次阅读 2019-01-14 22:49:19
    本文主要是关于第二章Java并发机制的底层实现原理的相关记录。主要包括volatile,synchronized,原子操作等实现原理的分析。 点击阅读更多系列文章我的个人博客–>幻境云图 1. 上下文切换 多线程 ...
  • volatile中是采用锁总线的方式实现可见性,在总线被锁期间其他CPU不可以访问主内存中变量,这种方式效率太低。 现在的可见性是由java和CPU共同完成的,利用CPU的缓存一致性来保证可见,原理如下: 要想理解可见性...
  • volatile实现原则: ·Lock前缀指令会引起处理器缓存回写到内存。(总线锁、缓存锁) ·一个处理器的缓存回写到内存会导致其他处理器的缓存无效。(MESI、嗅探) 缓存一致性协议: 处理器上有一套完整的协议,来...
  • 想知道 volatile 实现原理首先得去了下解 JMM,我们都知道 JVM 会为每一个 thread 开辟一块自己的工作空间,在我们操作变量时是从主内存拿到变量的一个副本,然后对副本进行操作后再刷新到主内存中这么一个总体的...
  • 若是共享变量是volatile声明的,就会把新值立即回写到主内存,通过store指令读取工作内存的变量传入到主内存,然后在主内存中通过write指令给变量赋值。 回写时底层通过lock前缀指令执行锁定这块内存区域的缓存...
  • 实现这些作用的底层如何实现Volatile能够保障可见性、有序性?原子性吗?前言我们都知道,Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,...
  • volatile底层原理详解

    2019-05-29 10:29:42
    今天我们聊聊volatile底层原理; Java语言规范对于volatile定义如下: Java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致性地更新,线程应该确保通过排它锁单独获得这个变量。 首先我们从定义...
  • 加了volatile后,编译的汇编语言中多了LOCK指令,那么这个指令的作用是什么呢? LOCK指令作用 将当前缓存行的内容写回到内存中,这个写回内存的操作会使得其他cpu里...因此volatile能够实现线程间数据的可见性。...
  • 那么今天我们来深入解析一下Volatile底层实现原理。 二、详细分析 1. 程序在计算机中如何执行的? 计算机的组成如下图所示: 当我们需要执行一个程序时,分为以下几步: (1) 将程序从磁盘中加载到...
  • 一、前言 之前我们学习过synchronized,知道它是一个重量级的锁,虽然jdk1.6对其做了很大的优化,但是...下面我们将深入剖析volatile实现原理。 二、什么是volatile Java语言规范第3版中对volatile的定义如下 Jav
  • 底层原理底层是通过lock前缀指令实现的,它会锁定该内存区域的缓存(缓存行锁定),并回写到主内存。 这个回写操作会使其他CPU里缓存了该内存地址的数据失效。 MESI缓存一致性协议 多个CPU从主内存中读取同一个...
  • 在并发编程中,synchronized 和 volatile 这两个关键字都扮演着重要的角色,volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的 “可见性”。 那么什么是 “可见性” 呢? 可见性的意思是当一...
  • 1.volatile的作用 1)保证线程间的可见性 2)防止指令重排 public class Test implements Runnable { boolean running = true; @Override public void run() { while(running){ } } public static void ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 43,754
精华内容 17,501
关键字:

volatile底层实现原理

友情链接: ILI9320_Initialize.rar