Volatile详解
一、Volatile介绍
Volatile是Java并发编程十分常见的关键字,它能保证被修饰元素的可见效和有序性,具体介绍之前,先来写一点相关的知识。
二、Java内存模型
java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如图
总结来说,线程首先对本地内存中主内存元素的复制进行操作,等操作完成后在把新的值写回主内存。 而在这个过程中如果有其他线程读取来主内存的值,它是不知道这个值已经被修改了,因为还没有被修改的线程写回主内存。
三、深入理解Volatile
被Volatile修饰的变量具有两个特性:
- 可见性:可见性指的是当线程对一个Volatile变量进行修改后,其他线程能立刻知道这个值已经被修改,结合上面Java内存模型来说,当一个线程对Volatile变量进行修改后,会立即将修改后的指写入主内存,而不是保存在本地内存
- 有序性 Volatile 可以禁止JVM对指令的重排
先来看一段代码:
//线程2
1 boolean flag = true; 2 while(flag){ 3 4 } 5 //线程1 6 flag = false;
假如线程2先执行,线程1后执行,再大部分情况下这段代码都会陷入死循环。
而当我们将 flag 用 Volatile 修饰,就可以避免死循环的产生,为什么?
1、线程1修改 flag 的结果会立刻写入内存
2、当线程1修改完 flag 后,线程2的本地变量失效,反映到硬件就是 CPU 对应的L1或者L2缓存行失效
3、失效后线程2又会重新读取 flag 值,所以不会出现死循环
Volatile能保证原子性吗?
先看一个例子:
1 class Test { 2 public volatile int inc = 0; 3 4 public void increase() { 5 inc++; 6 } 7 8 public static void main(String[] args) { 9 final Test test = new Test(); 10 for(int i=0;i<10;i++){ 11 new Thread(){ 12 public void run() { 13 for(int j=0;j<1000;j++) 14 test.increase(); 15 }; 16 }.start(); 17 } 18 19 while(Thread.activeCount()>1) //保证前面的线程都执行完 20 Thread.yield(); 21 System.out.println(test.inc); 22 } 23 }
这算一个经典的问题了,inc虽然是一个Volatile变量,但这个程序还是可能不能得到期望的结果,原因就在于 Volatile 并不能保证操作的原子性,特别是多元操作。
对于num++来说,底层是把它分为3步去做:
1、读取内存内num的指
2、进行+1操作
3、写入内存
在多线程去执行这个操作的时候,有时候会出现当前线程虽然读取了num的值,还没有进行其他操作,其他线程已经将num的值更改了,导致读取不到正确的值。
这里可能有个疑问前面不是说如果进行修改了会使缓存的数据无效嘛?
是会无效没错,但线程 1 进行读取后被阻塞还没有修改num的指,导致后来的线程2错误的认为没有被修改,所以线程 2 直接读取了内存的错误的指,导致了结果的错误,而这样的错误用 synchronized 和 Lock 都可以解决,所以 Volatile 是无法保证原子性的
Volatile能保证有序性是什么意思?
在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
Volatile功能实现的原理:
前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
四、Volatile的使用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1、状态标记
1 volatile boolean flag = false; 2 3 while(!flag){ 4 doSomething(); 5 } 6 7 public void setFlag() { 8 flag = true; 9 } 10 volatile boolean inited = false; 11 //线程1: 12 context = loadContext(); 13 inited = true; 14 15 //线程2: 16 while(!inited ){ 17 sleep() 18 } 19 doSomethingwithconfig(context);
2、Double Check
1 class Singleton{ 2 private volatile static Singleton instance = null; 3 4 private Singleton() { 5 6 } 7 8 public static Singleton getInstance() { 9 if(instance==null) { 10 synchronized (Singleton.class) { 11 if(instance==null) 12 instance = new Singleton(); 13 } 14 } 15 return instance; 16 } 17 }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战