【转】Java学习---volatile 关键字
【原文】https://www.toutiao.com/i6591422029323305480/
前言
不管是在面试还是实际开发中 volatile 都是一个应该掌握的技能。
首先来看看为什么会出现这个关键字。
内存可见性
由于 Java 内存模型(JMM)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。
线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。
这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存。
如下图所示:
所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。
显然这肯定是会出问题的,因此 volatile 的作用出现了:
当一个变量被 volatile 修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。
volatile 修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中。
内存可见性的应用
当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用 volatile 来修饰:
public class Volatile implements Runnable{
private static volatile boolean flag = true ;
@Override
public void run() {
while (flag){
}
System.out.println(Thread.currentThread().getName() +"执行完毕");
}
public static void main(String[] args) throws InterruptedException {
Volatile aVolatile = new Volatile();
new Thread(aVolatile,"thread A").start();
System.out.println("main 线程正在运行") ;
Scanner sc = new Scanner(System.in);
while(sc.hasNext()){
String value = sc.next();
if(value.equals("1")){
new Thread(new Runnable() {
@Override
public void run() {
aVolatile.stopThread();
}
}).start();
break ;
}
}
System.out.println("主线程退出了!");
}
private void stopThread(){
flag = false ;
}
}
主线程在修改了标志位使得线程 A 立即停止,如果没有用 volatile 修饰,就有可能出现延迟。
但这里有个误区,这样的使用方式容易给人的感觉是:
对 volatile 修饰的变量进行并发操作是线程安全的。
这里要重点强调,volatile 并不能保证线程安全性!
如下程序:
public class VolatileInc implements Runnable{
private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性
//private static AtomicInteger count = new AtomicInteger() ;
@Override
public void run() {
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet() ;
}
}
public static void main(String[] args) throws InterruptedException {
VolatileInc volatileInc = new VolatileInc() ;
Thread t1 = new Thread(volatileInc,"t1") ;
Thread t2 = new Thread(volatileInc,"t2") ;
t1.start();
//t1.join();
t2.start();
//t2.join();
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet();
}
System.out.println("最终Count="+count);
}
}
当我们三个线程(t1,t2,main)同时对一个 int 进行累加时会发现最终的值都会小于 30000。
这是因为虽然 volatile 保证了内存可见性,每个线程拿到的值都是最新值,但 count ++ 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。
- 所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。
- 也可以使用 synchronize 或者是锁的方式来保证原子性。
- 还可以用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性。
指令重排
内存可见性只是 volatile 的其中一个语义,它还可以防止 JVM 进行指令重排优化。
举一个伪代码:
int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3
一段特别简单的代码,理想情况下它的执行顺序是:1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3。
可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。
可能这里还看不出有什么问题,那看下一段伪代码:
private static Map<String,String> value ;
private static volatile boolean flag = fasle ;
//以下方法发生在线程 A 中 初始化 Map
public void initMap(){
//耗时操作
value = getMapValue() ;//1
flag = true ;//2
}
//发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
sleep() ;
}
//dosomething
doSomeThing(value);
}
这里就能看出问题了,当 flag 没有被 volatile 修饰时,JVM 对 1 和 2 进行重排,导致 value 都还没有被初始化就有可能被线程 B 使用了。
所以加上 volatile 之后可以防止这样的重排优化,保证业务的正确性。
指令重排的的应用
一个经典的使用场景就是双重懒加载的单例模式了:
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//防止指令重排
singleton = new Singleton();
}
}
}
return singleton;
}
}
这里的 volatile 关键字主要是为了防止指令重排。
如果不用 ,singleton = new Singleton();,这段代码其实是分为三步:
- 分配内存空间。(1)
- 初始化对象。(2)
- 将 singleton 对象指向分配的内存地址。(3)
加上 volatile 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。
总结
volatile 在 Java 并发中用的很多,比如像 Atomic 包中的 value、以及 AbstractQueuedLongSynchronizer 中的 state 都是被定义为 volatile 来用于保证内存可见性。
将这块理解透彻对我们编写并发程序时可以提供很大帮助。
【转】Java学习---volatile 关键字的更多相关文章
- 【转】java中volatile关键字的含义
java中volatile关键字的含义 在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...
- 转:java中volatile关键字的含义
转:java中volatile关键字的含义 在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...
- Java中Volatile关键字详解 (转自郑州的文武)
java中volatile关键字的含义:http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html 一.基本概念 先补充一下概念:J ...
- 【转】Java学习---Java中volatile关键字实现原理
[原文]https://www.toutiao.com/i6592879392400081412/ 前言 我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.c ...
- [面试必备]深入理解Java的volatile关键字
前言 在Java并发编程中,volatile关键字有着至关重要的作用,在面试中也常常会是必备的一个问题.本文将会介绍volatile关键字的作用以及其实现原理. volatile作用 volatile ...
- java中volatile关键字的含义
在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...
- Java transient volatile关键字(转)
Volatile修饰的成员变量在每次被线程访问时,都强迫从主内存中重读该成员变量的值.而且,当成员变量发生变化时,强迫线程将变化值回写到主内存.这样在任何时刻,两个不同的线程总是看到某个成员变量的同一 ...
- Java中Volatile关键字详解
一.基本概念 先补充一下概念:Java并发中的可见性与原子性 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值, ...
- java中volatile关键字的含义 (转载)
在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...
随机推荐
- 人人,金山西山居,腾讯互娱,微信,网易游戏offer及面经
转自:http://www.itmian4.com/forum.php?mod=viewthread&tid=3985 首先感谢师兄在两年前发的贴([天道酬勤] 腾讯.百度.网易游戏.华为Of ...
- openerp7 时区问题解决--改成本地时区
由于目前openerp的时区,读取的是UTC时间,而我国本地时间比UTC快8小时 问题就导致,写入数据库的时候时间相差8小时,以及Openerp日志输出时间格式也相差8小时和前端显示问题 1.更改op ...
- 解析Resources.arsc
一.前言 对于APK里面的Resources.arsc文件大家应该都知道是干什么的(不知道的请看我的另一篇文章Android应用程序资源文件的编译和打包原理),它实际上就是App的资源索引表.下面我会 ...
- normalize.css 中文版
## normalize.css 中文版 normalize.css 原地址:http://necolas.github.io/normalize.css/reset 太暴力了,这个 normaliz ...
- [转]单据套打WINFORM实现,带预览功能
本文转自:https://blog.csdn.net/lyflcear/article/details/22795053 昨天公司要打单子而不是以前的手写 为了实现这样的功能上网搜索了一下 http: ...
- php json_encode在CI框架中的使用细节
这个错误的造成原因是加载类类库,转换成json格式的时候不熟悉CI框架的规定导致的,CI框架中规定在将数据转换成json格式的时候需要将类库小写,当然了,调用的时候必须保证有这个类库,且可以在对应的文 ...
- Shiro眼皮下玩ajax,玩出302 Found
2017/06/14这一天,是我玩Shiro安全框架最刻骨铭心的一天.因为Shiro今天给我深深的补了一刀,在这儿我也给各位补一刀吧,其实问题很简单,解决方式也极其简单,只是给各位分享一下这个错误,纯 ...
- Spring MVC + Velocity实现国际化配置
国际化介绍 web开发中,国际化是需要考虑的一个问题,而且这个问题一般是越早敲定越好(不然等到系统大了,翻译是个问题).下面是结合实际项目(Spring MVC+Velocity)对实现国际化的一些总 ...
- BZOJ3884(SummerTrainingDay04-C 欧拉定理)
上帝与集合的正确用法 根据一些书上的记载,上帝的一次失败的创世经历是这样的: 第一天, 上帝创造了一个世界的基本元素,称做“元”. 第二天, 上帝创造了一个新的元素,称作“α”.“α”被定义为“元 ...
- 排序算法(8)--Merge Sorting--归并排序--Merge sort--归并排序
1.基本思想 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用.将已有序的子序列合并,得到完全有序的序列:即先使每个子序 ...