欢迎您的访问
专注于分享最有价值的互联网技术干货

Volatile 小白从入门到精通

几个T的资料等你来白嫖
双倍快乐

volatile 简介

volatile 关键字总是在各大面试中出现,下面我这边简单整理一下 volatile 到底是一个啥子东西?

废话不说下,我们看看volatile 在wikipedia 官方的 的解释,如图1:

20210413143605950.png

google翻译一下:在计算机编程语言过程中,针对C语言和java语言,volatile 关键字 表示的值
即使没有出现被修改的操作,会存在不同的入口可以修改其值。从而此关键字就可以阻止 optimizing compiler
【编译器优化】 后续的读或者写 操作造成的读取错误或者写入丢失的问题。

翻译比较绕口,说一下个人理解 :
在传统计算机中通信过程中,不同硬件设备对计算机存储的值进行会进行不同方式的持续读或者持续写操作,从而会导致数据不一致的现象比如 旧值重复读或者 新值覆盖等操作,因此衍生一个这样易失性关键字,被他标记的数据对其他硬件设备 或 线程具备可见性。
其实这个是一个通用的关键字,在不同的语言类型中,表示是不一样的:

  • 在C++、C中:类型限定符,例如const,并且是type的属性,通常用于设定变量不可更改或者只读
  • 在java 中 我们认为它是变量的属性,主要用于线程
public volatile int variable;

在上述英文描述中有一段 比较关键:
【prevents an optimizing compiler】阻止编译器优化

什么是阻止编译器优化呢?

我们拿Java举例吧,计算机由于性能原因,不同语言的编译器会拥有自己优化策略,因此计算机允许Java VM和CPU对程序中的指令进行重新排序 (优化策略之一),但是被Volatile标记的属性,计算机有他自己特殊的处理。

我们先看一下 从VM编译的角度看一下 ,Volatile标记的属性和 普通变量 有什么区别?

我们先看一下如下代码,如图2:
20210413143606204.png
我们用jdk 分别对两个代码进行编译一下 生成class 文件。

-- 生成class 文件
>javac  vl.java
>javac  vl2.java
------------------------
在通过反编译查看字节码文件
 > javap -verbose vl.class
 > javap -verbose vl2.class

比对观察字节码文件,如图 3
20210413143606737.png
不难发现,采用 Volatile 引用的变量 后面有一个 ACC_VOLATILE来表示,计算机在处理的时候会首先识别此标识类型,然后再判断类型后面的标识,通过不同的标识来决定是否遵循volatile的语义 来处理是否添加内存屏障来达到禁止指令重排的目的。【PS:内存屏障内容比较多,我会在后续的文章中补充】

Volatile 禁止指令重排

Volatile 有一个 核心特性:禁止指令重排
指令重排序定义
重排序是指编译器和处理器为了优化程序性能而对指令进行重新排序的一种手段。
指令重排序规则:

  1. 具有数据依赖性操作不能重排序
  2. 需要遵循 As-If-Serial 原则 【不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变】

什么是As-If-Serial ?
下面给大家举一个简单例子

示例 1
假设我们计算一个长方形面积。
int  length=6;          第一步
int  width=8;             第二步
int  area=length * width  =64;     第三步
-------------------------------------------------------------
length, width,和 area 是存在相互依赖关系的,第一、二步是不能放到第三步之前的会导致结果改变,因此不能重排序。

示例 2
-------------------------------
int a = 1; 
int b = 2; 
x++; 
y ++;
-------------------------------

重新排序以后
-------------------------------
int a = 1; 
y ++;
int b = 2; 
x ++;
-------------------------------
此时 重新排序没有什么大的影响,原因变量之间没有依赖关系。

再举一个列子,详细解释一下 Volatile 是如何禁止指令重排的
如图是:我们常用的单例模式:
单例模式: 一个类只有一个实例。
20210413143606940.png
上述代码 主要是进行创建一个单例示例:
我们都知道,在实例对象的时候 都会进行内存分配,如图:
20210413143607031.png
假设 没有禁止指令重排:

private  static Test test;

20210413143607355.png

结果会产生一个未实例化的test,如果采用了volatile
由于volatile 特性禁止指令重排,因此导致保证构造之后再修改变量引用。

从硬件的角度看:
在计算机中,指令重排是指CPU采用了允许将多条指令不按照程序规定的顺序分开发送给各个相应电路单元处理,但并不是说指令的任意重排,CPU需要能够正确处理指令的依赖情况,以保障程序能够输出正确的结果 。—— 出自《深入理解java 虚拟机 12章》

除此之外:
在wikipedia 、其他英文博客文档中 老外还提到一个Volatile的特性 英文如下:

20210413143607446.png
20210413143607517.png
大致意思:
Volatile 对象可以实现未知的方式意外更改
大概什么意思呢?我这边简单解释一下
我们先看一下如下代码:
20210413143607821.png
我们可以看一下,当前代码
一个是普通成员变量引用的,另一个采用 Volatile 变量引用
此时如果 执行当前左边的方法,此时方法体会进入无限循环直到程序溢出。
右侧的程序 也是无限循环直到溢出,但是区别是,右侧程序可以在程序外部更改并且中断当前循环,从而达到对未知的方式意外更改的作用。

通过上述的例子我们可以发现Volatile 有一个使用场景:

  • Volatile 可以用于特定实现一个 状态控制标识.

例子:
可以从JMX侦听器,GUI事件线程中的操作侦听器,通过RMI,通过Web服务等调用它.
例如:
代码主要功能实现一个多线程服务状态监听器。

volatile boolean shutdownRequested;

...
 //该方法可能shutdown()会从循环外的某个位置(在另一个线程中)被调用,从而控制所有线程的状态
public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

Volatile可见性

可见性定义:当前线程修改了次值,其他线程立即产生变化或者获取最新的结果。

20210413143608074.png
普通变量 与 Volatile 区别
普通变量
在多线程应用程序中,线程对普通变量进行操作,最早的计算机是在内存/硬盘中读取/写入的变量,随着时间的推移,计算机高速缓存出现,出于性能方面的考虑,每个线程在对其进行处理时,可以将变量从主内存复制到工作缓存中【Java Stack Cache】,对于多核CPU计算机,则每个线程可能在不同的CPU上运行。这意味着每个线程将变量复制到不同CPU的工作缓存中。
当线程A 修改一个普通变量的值,需要向主内存进行回写。另一个线程B在A线程写完成以后,再从主内存进行读取操作,新变量值才会对线程B可见。
这样就会造成一个问题。普通变量不能保证Java虚拟机(JVM)何时将数据从主存储器读取到CPU缓存中,或何时将数据从CPU缓存写入主存储器。因此就会造成我们常说的不一致问题。
Volatile变量
A线程 对 volatile 变量 进行的写操作并立即写回到主存储器。此时线程B就能立即获取到最新的值。

Synchronized VS Volatile 区别:

比较之前,我们先说一下,线程安全两个重要方面:

  • 执行流程控制
  • 内存可见性问题

执行流程控制 :

  • 控制代码何时处理
  • 控制哪一个命令按照流程处理
  • 控制是否能够同时并发处理

内存可见性 :
操作对内存已经完成的结果,对其他线程具备可见性。
由于每个CPU与主内存之间都有若干级缓存,因此运行在不同CPU或内核上的线程 在每个时刻都可以看到不同的“内存”,因为允许线程获取并使用主内存的自己私有副本。

  • synchronized是方法级别/块级别访问限制修饰符。它将确保一个线程持自己的有关键部分(方法或者代码块)的锁。只有拥有锁的线程才能进入synchronized块。如果其他线程试图访问此关键部分,则它们必须等到当前所有者释放锁为止。
  • volatile是变量访问修饰符,它强制所有线程从主内存中获取变量的最新值。不需要锁定即可访问volatile变量。所有线程都可以同时访问volatile变量值
  • volatile主要解决 :缓存/过时的内存问题和编译器和CPU优化问题,但是它不解决 线程竞争问题,synchronized由于控制的竞争条件从而能够解决此这三类类问题。
    举一个例子:
int x=0;
x++;
-------------------------
synchronized (this) {
   x++; // no problem now
}

当前操作 如果在多线程情况,x 的结果可能在不通的线程里输出不一样。
此时添加了同步锁以后,当前操作就原子化,一个线程持有锁,如果不释放锁的话,另一个线程就无法进行干扰。那首先他就解决了竞争问题
上文中提到每个线程都存在自己的缓存结构,他们可以在自己的缓存中修改值,由于当前代码块被锁定因此,当前值的缓存不会出现在其他线程共享的情况。从而解决 缓存/过时的内存编译器和CPU优化问题

因此:
被volatile标记变量的作用是所有线程仅在主内存上执行读取和写入操作
synchronized的作用是 每个线程在进入该块时都要从主内存中更新其值,并在释放锁时将结果刷新回主存中。

总结

  • 当变量将要被多个线程读取但只能由一个线程写入时,请使用Volatile
  • 当变量将被多个线程读写时,请使用Synchronized

Volatile常用使用场景:

  1. volatile假设有一个环境传感器可感测当前温度。后台线程可能每隔几秒钟读取一次此传感器,并更新包含当前温度的volatile变量。然后,其他线程可以知道该变量始终会看到最新的值来读取该变量。
  2. Volatile Bean结构 Bean 属性对象成员变量,比如Date日期,所有访问此变量的线程始终从主内存中获取最新数据,以便所有线程都显示真实(实际)日期值。
  3. 接口回调使用比如 使用一个程序来注册一个或多个回调函数,回调是由程序中的线程调用的,可以统计信息volatile声明变量,可以保证运行测试代码的线程将看到值的变化。

相关参考资料:https://www.ibm.com/developerworks/java/library/j-jtp06197/index.html
https://stackoverflow.com/questions/3519664/difference-between-volatile-and-synchronized-in-java
https://loonytek.com/2019/08/18/volatile-and-synchronized/

赞(0) 打赏
版权归原创作者所有,任何形式转载请联系我们:大白菜博客 » Volatile 小白从入门到精通

评论 抢沙发

0 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏