前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java Volatile关键字

Java Volatile关键字

原创
作者头像
HLee
修改2021-10-18 14:39:44
5160
修改2021-10-18 14:39:44
举报
文章被收录于专栏:房东的猫房东的猫

前言

volatile关键字只能修饰类变量和成员变量,对于方法参数、局部变量以及实例常量、类常量都不能进行修饰。

机器硬件CPU

在计算机中,所有的运算操作都是由CPU的寄存器来完成的,CPU 指令的执行过程需要涉及数据的读取和写人操作,CPU所能访问的所有数据只能是计算机的主存(通常是指RAM),虽然CPU的发展频率不断地得到提升,但受制于制造工艺以及成本等的限制,计算机的内存反倒在访问速度上并没有多大的突破,因此CPU的处理速度和内存的访问速度之间的差距越拉越大,通常这种差距可以达到上千倍,极端情况下甚至会在上万倍以上。

在CPU中至少要有六类寄存器:指令寄存器(IR)、程序计数器(PC)、地址寄存器(AR)、数据寄存器(DR)、累加寄存器(AC)、程序状态字寄存器(PSW)。

在计算机中,所有的运算操作都是由CPU的寄存器来完成的,CPU指令的执行过程需要涉及数据的读取和写入操作,CPU所能访问的所有数据只能是计算机的主存(通常是指RAM)。

  • RAM(random access memory)即随机存储内存,这种存储器在断电时将丢失其存储内容,故主要用于存储短时间使用的程序。
  • ROM(Read-Only Memory)即只读内存,是一种只能读出事先所存数据的固态半导体存储器。

手机中的RAM和ROM分别对应电脑的内存和硬盘

CPU 缓存模型

由于两边速度严重不对等,通过传统的FSB直连内存的访问方式很明显会导致CPU资源受到大量的限制,降低CPU整体的吞吐量,于是就有了CPU和主内存之间增加缓存的设计,现在缓存的数量可以增加到3级了,最接近CPU的缓存称为L1,然后依次是L2、L3和主内存。

由于程序指令和程序数据的行为和热点分布差异很大,因此L1 Cache又被划分成了L1i (i是instruction的首字母)和L1d (d是data的首字母)这两种有各自专门用途的缓存,CPU Cache又是由很多个Cache Line构成的,Cache Line可以认为是CPU Cache中的最小缓存单位,目前主流CPU Cache的Cache Line大小都是64字节。

Cache的出现是为了解决CPU直接访问内存效率低下的问题,程序在运行过程中,会将运算所需要的数据从主存复制一份到CPU Cache中,这样CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写入,当运算结束之后,再将CPU Cache中的最新数据刷新到主存当中,CPU通过直接访问Cache的方式替代直接访问主存的方式极大提高了CPU的吞吐能力。

CPU缓存一致性

由于缓存的出现,极大地提高了CPU的吞吐能力,但是同时也引入了缓存不一致性的问题,比如i++操作,在程序运行过程中,首先需要将主内存中的数据复制一份到存放CPU Cache中,那么CPU寄存器在进行数值计算的时候就直接到Cache中读取和写入,当整个过程运算结束之后再将Cache中的数据刷新到主存中。

  1. 读取主内存的i到CPU Cache中
  2. 对i进行加一操作
  3. 将结果写回到CPU Cache中
  4. 将数据刷新到主内存中

i++在单线程的情况下不会出现任何问题,但是多线程下就会有问题,每个线程都有自己的工作内存(本地内存,对应CPU中的Cache),变量i会在多个线程的本地内存中都存在一个副本。如果同时有两个线程执行i++操作,那么就会出现经典的缓存不一致问题。

为了解决缓存不一致问题,通常主流的解决方法有如下两种:

  • 通过总线加锁的方式
  • 通过缓存一致性协议

第一种方式常见于早期的CPU当中,而且是一种悲观的实现方式。

第二种缓存一致性协议中最出名的是Intel的MESI协议,MESI协议保证了每一个缓存中使用的共享变量副本都是一致的,它的大概意思是,当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量,也就是说在其他的CPU Cache中存在一个副本,那么进行如下操作:

  • 读取操作,不做任何处理,只是将Cache中的数据读取到寄存器中。
  • 写入操作,发出信号通知其他CPU将该变量的Cache line置为无效状态,其他CPU再进行该变量读取的时候不得不到主内存中再次获取。

Java内存模型

Java内存模型(Java Memory Mode,JMM)指定了Java虚拟机如何与计算机的主存(RAM)进行工作。

Java内存模型决定了一个线程对共享变量的写入何时对其他线程可见,Java内存模型定义了线程和主内存之间的抽象关系,具体如下:

  • 共享变量存储于主存之中,每个线程都可以访问
  • 每个线程都有私有的工作内存或者称为本地内存
  • 工作内存只存储该线程对共享变量的副本
  • 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
  • 工作内存和Java内存模型一样也是一个抽象概念,它其实并不存在,它涵盖了缓存、寄存器、编译期优化以及硬件等

假设主内存的共享变量为0,线程1和线程2分别拥有共享变量X的副本,假设线程1此时将工作内存中的x修改为1,同时刷新到主内存中,当线程2想要去使用副本x的时候,就会发现该变量已经失效了,必须到主内存中再次获取然后存人自己的工作内容中,这一点和CPU与CPU Cache之间的关系非常类似。

Java的内存模型是一个抽象的概念,与其计算机硬件的结构并不完全一样,比如计算机物理内存不会存在栈内存和堆内存的划分,无论是堆内存还是虚拟机栈内存都会对应到物理的内存,当然也有一部分堆内存的数据可能存入CPU Cache寄存器中。

并发编程重要特性

并发编程的三个至关重要的特性:分别是原子性、有序性、可见性。

一般来说,处理器为了提高程序的运行效率,可能会对输入的代码指令做一定的优化,它不会百分之百的保证代码的执行顺序严格按照编写代码中的顺序来进行,但是它会保证程序的最终运行结果是编码时所期望的那样。

原子性

所谓原子性是指一次性的操作或者多次操作中,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。

总结:volatile关键字不能保证原子性,synchronized关键字可以保证数据的原子性

可见性

所谓可见性是指当一个线程对变量进行了修改,那么另外的线程可以立即看到修改后的最新值。

有序性

所谓有序性是指程序代码在执行过程中的先后顺序,由于Java在编译器以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序,比如:

代码语言:javascript
复制
intx=10;
int y =0;
x++ ;
y= 20;

上面这段代码定义了两个int类型的变量x和y,对x进行自增操作,对y进行赋值操作,从编写程序的角度来看上面的代码肯定是顺序执行下来的,但是在JVM真正地运行这段代码的时候未必会是这样的顺序,比如y=20语句有可能会在x++语句的前面得到执行,这种情况就是我们通常所说的指令重排序(Instruction Recorder)。

一般来说,处理器为了提高程序的运行效率,可能会对输人的代码指令做一定的优化,它不会百分之百的保证代码的执行顺序严格按照编写代码中的顺序来进行,但是它会保证程序的最终运算结果是编码时所期望的那样,比如上文中的x++与y=20不管它们的执行顺序如何,执行完上面的四行代码之后得到的结果肯定都是x=11, y=20。

当然对指令的重排序要严格遵守指令之间的数据依赖关系,并不是可以任意进行重排序的,比如下面的代码片段:

代码语言:javascript
复制
intx=10;
inty=0;
x++ ;
y=x+1;

对于这段代码有可能它的执行顺序就是代码本身的顺序,有可能发生了重排序导致int y=0优先于int x=10执行,但是绝对不可能出现y=x+1优先于x++执行的执行情况,如果一个指令x在执行的过程中需要用到指令y的执行结果,那么处理器会保证指令y在指令x之前执行,这就好比y=x+1执行之前肯定要先执行x++一样。

在单线程情况下,无论怎样的重排序最终都会保证程序的执行结果和代码顺序执行的结果是完全一致的,但是在多线程的情况下,如果有序性得不到保证,那么很有可能就会出现非常大的问题,比如下面的代码片段:

代码语言:javascript
复制
private boolean initialized = false; // 成员变量
private Context context;

public Context load() {
    if(! initialized){
        context=loadContext( ) ;
        initialized = true;
    }
    return context ;
}

上述这段代码使用boolean变量initialized来控制context是否已经被加载过了,在单线程下无论怎样的重排序,最终返回给使用者的context都是可用的。如果在多线程的情况下发生了重排序,比如context=loadContext()的执行被重排序到了initialized = true 的后面,那么这将是灾难性的了。比如第一个线程首先判断到initialized=false,因此准备执行context的加载,但是它在执行loadContext()方法之前二话不说先将initialized置为true然后再执行loadContext() 方法,那么如果另外一个线程也执行load方法,发现此时initialized已经为true了,则直接返回一个还未被加载成功的context,那么在程序的运行过程中势必会出现错误。

JMM保证三大特性

JVM采用内存模型的机制来屏蔽各个平台和操作系统之间内存访问的差异,以实现让Java程序在各种平台下达到一致的内存访问效果,比如C语言中的整型变量,在某些平台下占用了两个字节的内存,在某些平台下则占用了四个字节的内存,Java则在任何平台下,Int类型就是四个字节,这就是所谓的一致内存访问效果。

Java的内存模型规定了所有的变量都是存在于主内存(RAM)当中的,而每个线程都有自己的工作内存或者本地内存(这一点很像CPU的Cache),线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接对主内存进行操作,并且每一个线程都不能访问其他线程的工作内存或者本地内存。比如在某个线程中对变量i的赋值操作i=1,该线程必须在本地内存中对i进行修改之后才能将其写人主内存之中。

JMM与原子性

在Java语言中,对基本数据类型的变量读取赋值操作都是原子性的,对引用类型的变量读取和赋值的操作也是原子性的因此诸如此类的操作是不可被中断的,要么执行,要么不执行,正所谓一荣俱荣一损俱损。

(1) x=10; 赋值操作

x=10的操作是原子性的,执行线程首先会将x=10写人工作内存中,然后再将其写人主内存(有可能在往主内存进行数值刷新的过程中其他线程也在对其进行刷新操作,比如另外-一个线程将其写为11, 但是最终的结果肯定要么是10,要么是11,不可能出现其他情况,单就赋值语句这一一点而言其是原子性的)。

(2) y=x; 赋值操作

这条操作语句是非原子性的,因为它包含如下两个重要的步骤。

  • 执行线程从主内存中读取x的值(如果x已经存在于执行线程的工作内存中,则直接获取)然后将其存人当前线程的工作内存之中。
  • 在执行线程的工作内存中修改y的值为x,然后将y的值写人主内存之中。

虽然第一步和第二步都是原子类型的操作,但是合在一-起就不是原子操作了。

(3) y++; 自增操作

这条操作语句是非原子性的,因为它包含三个重要的步骤,具体如下。

  • 执行线程从主内存中读取y的值(如果y已经存在于执行线程的工作内存中,则直接获取),然后将其存人当前线程的工作内存之中。
  • 在执行线程工作内存中为y执行加1操作。
  • 将y的值写人主内存。

综合上面的例子,我们可以发现只有第- -种操作即赋值操作具备原子性,其余的均坏具备原子性,由此我们可以得出以下几个结论。

  1. 多个原子性的操作在一起就不再是原子性操作了。
  2. 简单的读取与赋值操作是原子性的,将一个变量赋给另外一个变量的操作不是原子性的。
  3. Java内存模型( JMM)只保证了基本读取和赋值的原子性操作,其他的均不保证,如果想要使得某些代码片段具备原子性,需要使用关键字synchronized,或者JUC中的lock。如果想要使得int等类型自增操作具备原子性,可以使用JUC包下的原子封装类型java.util.concurrent.atomic.*

总结:volatile关键字不具备保证原子性的语义。

JMM与可见性

在多线程的环境下,如果某个线程首次读取共享变量,则首先得到主内存中获取该变量,然后存入工作内存中,以后只需要在工作内存中读取该变量即可。同样如果对该变量执行了修改的操作,则先将新值写入工作内存,然后再刷新到主内存中。但是什么时候最新的值会被刷新到主内存是不太确定了。

Java提供了三种方式来保证可见性:

  • 使用关键字volatile:当一个变量被volatile修饰时,对于共享资源的读操作会直接在主内存中进行(当然也会缓存到工作内存中,当其他线程对该贡献资源进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然是先要修改工作内存,但是修改结束后会立即将其刷新到主内存中。
  • 通过synchronized关键字能够保证可见性,synchronized关键字能够保证同一时刻只有一个线程获得锁,然后执行同步方法,并且还会确保在锁释放前,会将对变量的修改刷新到主内存当中。
  • 通过JUC提供的显式锁Lock也能够保证可见性,Lock的lock方法能够保证在同一时刻只有一个线程获得锁然后执行同步方法,并且会确保在锁释放(Lock的unlock方法)之前会将对变量的修改操作刷新到主内存中。

总结:volatile关键字可以保证可见性

JMM与有序性

在Java内存模型中,允许编译期和处理器对指令进行重排,在单线程先重排序不会引起什么问题,但是多线程下,重排序会影响到程序的正确运行,Java提供了三种保证有序性的方式。

  • 使用volatile关键字来保证有序性
  • 使用synchronized关键字来保证有序性
  • 使用显示锁Lock来保证有序性

总结:volatile关键字可以保证有序性

Volatile关键字

被volatile修饰的实例变量或者类变量具备如下两层含义:

  • 保证了不同线程之间对共享变量操作时的可见性,也就是说当一个线程修改volatile修饰的变量,另一个线程会立即看到最新的值
  • 禁止对指令进行重排序操作

理解volatile保证可见性

  1. Reader线程从主内存中获取init value的值为0,并且将其缓存到本地工作内存中。
  2. Updater 线程将init_ value 的值在本地工作内存中修改为1,然后立即刷新至主内存中。
  3. Reader线程在本地工作内存中的init_ value 失效(反映到硬件上就是CPU的L1或者L2的Cache Line失效)。
  4. 由于Reader线程工作内存中的init_value失效,因此需要到主内存中重新读取init_value的值。

经过上面几个步骤的分析,相信读者对volatile关键字保证可见性有了一个更加清晰的认识了。

理解volatile保证有序性

volatile关键字对顺序性的保证就比较霸道一点,直接禁止JVM和处理器对volatile关键字修饰的指令重排序,但是对于volatile前后无依赖关系的指令则可以随便怎样排序。

理解volatile不保证原子性

volatile 关键字不保证操作的原子性。

volatile的原理和实现机制

  • 确保指令重排序时不会将其后面的代码排到内存屏障之前。
  • 确保指令重排序时不会将其前面的代码排到内存屏障之后。
  • 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成。
  • 强制将线程工作内存中值的修改刷新至主内存中。
  • 如果是写操作,则会导致其他线程工作内存(CPU Cache)中的缓存数据失效。

Volatile和Synchronized

虽然volatile有部分synchronized关键字的语义,但是volatile不可能完全替代synchronized关键字,因为volatile 关键字不具备原子性操作语义,我们在使用volatile关键字的时候也是充分利用它的可见性以及有序性(防止重排序)特点。

1. 使用上的区别

  • volatile关键字只能用于修饰实例变量或者类变量。
  • synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块。
  • volatile修饰的变量可以是null,synchronized关键字同步语句块的monitor对象不能为null。

2. 对原子性的保证

  • volatile无法保证原子性。
  • 由于synchronized是一种排他的机制,因此被synchronized关键字修饰的同步代码时无法被中途打断的,因此其能够保证代码的原子性。

3. 对可见性的保证

两者均可以保证共享资源在多线程间的可见性,但是实现机制完全不同。synchronized借助于JVM指令monitor enter和monitor exit对通过排他的方式使得同步代码串行化,在monitor exit时所有共享资源都将会被刷新到主内存中。相比较synchronized关键字volatile使用机器指令(硬编码)“lock;”的方式迫使其他线程工作内存中的数据失效,不得到主内存中进行再次加载。

4. 对有序性的保证

volatile关键字禁止JVM编译期以及处理器对其进行重排序,所以它能够保证有序性。虽然synchronized关键字所修饰的同步方法也可以保证有序性,但是这种有序性是以程序串行化执行换来的,在synchronized关键字修饰的代码块中代码指令也会发生指令重排序的情况。

5. 其他

  • volatile不会使线程陷入阻塞
  • synchronized关键字会是线程陷入阻塞

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 机器硬件CPU
    • CPU 缓存模型
      • CPU缓存一致性
      • Java内存模型
      • 并发编程重要特性
        • 原子性
          • 可见性
            • 有序性
            • JMM保证三大特性
              • JMM与原子性
                • JMM与可见性
                  • JMM与有序性
                  • Volatile关键字
                    • 理解volatile保证可见性
                      • 理解volatile保证有序性
                        • 理解volatile不保证原子性
                          • volatile的原理和实现机制
                            • Volatile和Synchronized
                              • 1. 使用上的区别
                              • 2. 对原子性的保证
                              • 3. 对可见性的保证
                              • 4. 对有序性的保证
                              • 5. 其他
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档