JMM,Volatile关键字与Java多线程

0.引入

         在开始讨论Volatile关键字与Java多线程之前,我们先回顾一下Java中线程的通信知识:①线程之间可以通过共享内存或基于网络进行通信(使用共享内存要求线程在同一主机下,不同主机之间的线程需要使用网络进行通信);②如果是通过共享内存的方式通信就要考虑并发,阻塞,唤醒问题。在Java中使用wait(),notify()可以实现阻塞与唤醒;③在网络通信时考虑并发问题主要通过加锁解决。

          Tip:本博客对应学习视频,图文出处https://www.bilibili.com/video/BV16q4y1W7Fy?p=10&spm_id_from=pageDriver

1.Java多线程内存模型

         了解Java多线程内存模型,我们需要先了解一个概念:JMM。JMM全称Java Memory Model,即Java内存模型,我们可以简单的把它理解为运行Java程序时, CPU与内存之间的交互模式(提示:JMM与JVM是两者之间基本上没有什么关系,防止记混淆)。既然已经知道JMM主要负责Java程序CPU与内存之间的交互模式,我们不妨先看一下计算机内部的多核并发缓存架构,如下图:

                                        

            我们可以看出,计算机多核并发缓存架构是有CPU,CPU缓存,RAM三个部分组成的,其中CPU缓存的存在是为了缓解CPU与RAM之间运行速度差异过大的问题。而在JMM中,Java多线程内存模型是基于 CPU缓存模型建立的,JAVA线程内存模型是标准化的,屏蔽了底层不同计算机的差异,具体结构如下图所示:

                    

            从上图我们可以看出,Java的多个线程在使用主内存的共享变量时不会直接使用变量,而是从主内存中获取变量然后将其拷贝到自己的工作内存中生成共享变量副本,之后线程对共享变量的操作都是对这一副本进行的,当线程结束后,再将改变过的副本值重新写回共享变量。这就是Java多线程内存模型。

            从上述模型中我们很容易就会发现问题,比如有一个int a=2的共享变量先后被线程A,B获取复制到自己的变量副本中,A的执行速度较快,它将a的值变为了3然后写回共享变量中;而B得执行速度较慢,在A已经写回共享变量值后,它还在执行,但它此时使用的依旧是自己副本中记录的a=2,这样就产生了多线程数据不一致的问题。我们使用下面的代码来展示这个问题:

             上述代码定义一个initFlag=false;的初始变量,执行程序后结果如下:

              我们发现程序永远不会输出“==========success”,可见线程2修改的共享变量值永远无法被线程1感知到,因为它一直使用的是自己工作内存中的变量副本。

2.Volatile关键字

           对于上面提到的问题,我们只需要在定义initFlag变量前加上volatile关键字即可。关于为什么volatile可以实现做到这一点,通常我们会回答volatile可以保证数据的可见性,保证有volatile变量修饰的变量发送改变时,所有使用改变量的线程都会收到通知从而更新自己工作内存中该变量的副本值。那么,volatile是如何做到让所有线程都得到变量发送更改这一通知的呢?

             这里,我们就又要回到JMM中去了,首先我们要知道,JMM不仅为我们提供了上图的Java多线程内存模型,还为我们提供了以套原子指令操作,如下图所示:

             这套指令其实就是具体实现Java多线程内存模型图里面的各种操作,如读取主内存中共享变量到线程工作内存,写回等操作。 出现数据不一致时其指令流程如下图:

            我们可以看出,线程2重新写入主内存数据后线程1并没有能够获取到改变,那么加入volatile关键字后为什么线程1可以知道主内存共享变量发生改变?主要是因为volatile会为我们开启总线嗅探机制和缓存一致协议(MESI) 。

            我们都知道,CPU与内存进行数据交流要通过总线,而总线嗅探机制在监听到共享变量发生变化后会将其他线程副本内存中数据失效,此时线程需要调用这个变量就只能从内存中重新获取。而volatile之所以能帮我们实现这些,是因为它的底层实现使用汇编语言写的,它使用了汇编语言中的lock这一命令。我们查看上述代码的汇编语言版本可以发现以下部分:

            上图中resp是寄存器,这段命名就是在为volatile修饰的变量进行赋值,如果没有volatile就不会有lock命令。关于lock指令的具体功能如下:

        对于1)的理解,正常情况下线程会在执行完后才会写回数据,而lock会在数据发生变化后立即写回主内存,同时出发总线嗅探机制和缓存一致协议,能保证时效性。  有关总线嗅探机制与缓存一致协议请参考其它博文理解。

3.Volatile其它内容

         并发编程有三大特性:可见性,有序性,原子性。关于可见性我们第二部分已经叙述过了,而volatile换可以实现有序性,但无法实现原子性,原子性需要使用锁机制等实现。而有关有序性就涉及到另一个概念,指令重排序。

         指令重排序是指在不影响单线程程序执行结果的情况下,计算机为了最大限度的提高运行效率,会对机器指令进行重排序。

         

        重排序会遵循as-if-serial和happens-befor原则。简单来说,计算机在执行源代码前会对源代码进行语意分析,生成语法树,判断语句之间有没有依赖关系,如果没有依赖关系就代表这两条语句可以进行重排序,不会影响单线程执行结果。用简单的代码说明,{x=1;y=1}这两句代码是可以重排序的,因为不论谁前谁后都不影响结果,而{x=1;y=x;}就不能重排序,因为x的赋值会影响后续y的值。

但种语意分析是在单线程内部的.如果多线程进行就可能出错,将语法进行重排序。为解决这个问题,就有了内存屏障这个想法。

        内存屏障其实就是要告诉计算机那些代码不要进行重排序。JVM中对内存屏障管理如下:

         而volatile关键字规范要求要实现这些内存屏障,而其具体的实现依旧是使用汇编lock指令,其内部源码如下:


4.as-if-serial和happens-befor原则

 

 

 

             

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值