想要理解volatile关键字,你只需要掌握它的这三个特点
Posted 杀手不太冷!
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了想要理解volatile关键字,你只需要掌握它的这三个特点相关的知识,希望对你有一定的参考价值。
想要理解volatile关键字,你只需要掌握它的这三个特点
volatile关键字可以保证被修饰变量的可见性
要想理解volatile关键字,得先了解下JAVA的内存模型,Java内存模型的抽象示意图如下:
从图中可以看出每一个线程,都有一个本地内存,操作共享变量的时候,线程执行时,会先把主内存中的共享变量拷贝一份到每一个线程的本地内存中,然后再对该共享变量进行操作。等到操作完成之后,在某个时刻会把本地内存中的操作共享变量的结果刷新到主内存中。
能够体现volatile关键字修饰的变量可以被所有的线程看见的例子,如下图:
上图程序的运行结果,如下图:
但是如果要是不用volatile关键字修饰标志变量continuePrint,如下图:
去掉volatile关键字之后的运行结果,如下图:
去掉volatile关键字之后,Thread-0线程中接收不到main线程中修改标志变量continuePrint后的结果的原因是:main线程和Thread-0线程都有一个本地内存,刚开始的时候,标志变量continuePrint的值是true,这个值存放在主内存中,main线程和Thread-0线程执行的时候,会从主内存把continuePrint=true复制一份到它们的本地内存之中,它们操作各自本地内存之中的continuePrint标志变量的时候互不影响,在main线程中把continuePrint的值改成false,就仅仅是在main线程的本地内存中把continuePrint的值改成了false,不会影响Thread-0线程本地内存中的continuePrint的值,它的值仍然是true,因此Thread-0线程中的while循环不会终止。
加上volatile关键字之后,Thread-0线程中可有接收到main线程中修改结果的原因是:如果用volatile关键字修饰continuePrint标志变量,那么线程main和线程Thread-0在读取continuePrint标志变量的时候,不会从它们的本地内存中读取,而是会从主内存中读取;在读取之后,写入continuePrint标志变量的时候,main线程和Thread-0线程也会直接把continuePrint标志变量的值写入到主内存中,而不是写入到它们的本地内存之中。因此,这样的话,在main线程中把continuePrint标志变量的值改成false之后,在Thread-0线程中是可以读取到的。
volatile不能保证修饰变量的原子性
什么叫做原子性,就是某系列的操作步骤要么全部执行,要么都不执行,比如:
i++这句代码其实对应的JVM字节码指令有三条,第一条是从内存中取出i的值,第二条是对i进行加一,第三条是把加一后的i存入内存中,如果这三条指令不能同时执行,这就叫做不能保证i++这句代码的原子性;
volatile不能保证被修饰的变量的原子性的例子,如下图:
volatile不能保证被修饰变量的原子性,其实也就是说明了volatile不能保证被修饰变量的线程安全。
volatile可以避免指令重排
什么是指令重排?
你比如有两句代码int a=1;int b=2;指令重排之后可能就变成了int b=2;int a=1;但是指令重排并非可以任意的重排,也是有限制的,比如有三句代码int a=1;int b=a+1;int c=3;因为b依赖a,所以int b=a+1;这句代码就不能排在int a=1;的前面,但是c不依赖a和b的任何一个,所以int c=3这句代码可以重排的任意位置;
指令重排有什么用?
指令重排可以提高cpu处理器的效率,为什么这样说呢?你比如,有两个cpu处理器A,B
处理器A执行代码 变量a++ 变量b++
处理器B执行代码 变量a++ 变量b++
但是如果按这样的顺序执行的话,那么在A处理器执行 变量a++的时候,B处理器就必须先停止,这样也就浪费了cpu资源;
但如果进行一下指令重排,把处理器B执行的代码重排成 变量b++ 变量a++,那么在处理器A执行变量a++操作的时候,处理器B就不需要停止了,它可以执行变量b++操作,这样的话也就提高了cpu的效率。
**volatile关键字修饰的变量不会被指令重排序优化。**这里以《深入理解JAVA虚拟机》中一个例子来说明下自己的理解:
线程A执行的操作如下:
Map configOptions ;
char[] configText;
volatile boolean initialized = false;
//线程A首先从文件中读取配置信息,调用process...处理配置信息,处理完成了将initialized 设置为true
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfig(configText, configOptions);//负责将配置信息configOptions 成功初始化
initialized = true;
线程B等待线程A把配置信息初始化成功后,使用配置信息去干活…线程B执行的操作如下:
while(!initialized)
{
sleep();
}
//使用配置信息干活
doSomethingWithConfig();
如果initialized变量不用 volatile 修饰,在线程A执行的代码中就有可能指令重排序。
即:线程A执行的代码中的最后一行:initialized = true 重排序到了 processConfig方法调用的前面执行了,这就意味着:配置信息还未成功初始化,但是initialized变量已经被设置成true了。那么就导致 线程B的while循环“提前”跳出,拿着一个还未成功初始化的配置信息去干活(doSomethingWithConfig方法)。。。。
因此,initialized 变量就必须得用 volatile修饰。这样,就不会发生指令重排序,也即:只有当配置信息被线程A成功初始化之后,initialized 变量才会初始化为true。综上,volatile 修饰的变量会禁止指令重排序(有序性)
以上是关于想要理解volatile关键字,你只需要掌握它的这三个特点的主要内容,如果未能解决你的问题,请参考以下文章