1.Volatile关键字是什么?
2.Volatile关键字的作用
2.1 JMM(JAVA内存模型)的理解
2.2 Volatile的保证可见性
2.3 Volatile禁止指令重排序
3.用volitile改写单例模式
1.Volatile关键字是什么?
我们都知道synchronize关键字,是一种重量型的同步机制,相对而言,volitile是一种轻量级的同步机制,volatile字面有“易挥发”的意思,Volatile关键字用于修饰共享可变变量,因为被修饰的值容易变化(容易被其它线程更改),因为不确定。volatile它有与锁相同的作用:保证可见性和有序性,所不同的是,在原子性方面只保证写变量操作的原子性,单没有锁排他性。
2.Volatile关键字的作用
1.保证可见性
2.不保证原子性
3.禁止指令重排
我们先解释第一点,保证可见性,可见性我们要从JMM(JAVA内存模型)开始讲起
2.1 JMM(JAVA内存模型)的理解
JMM并不真实存在,它描述的是一组规范
JVM工作线程工作的时候,是有自己的工作内存的,工作内存是每个线程的私有区域,而java所有变量都保存在主存,主存是共享区域。线程在运行的时候,会先把变量拷贝一份到自己的工作内存,然后对变量进行赋值操作,操作完成后再将变量写会主存。并不能直接操作主内存变量,各个线程的工作内存中存储着主存的变量副本拷贝,因此不同的线程无法访问对方的工作主存,线程的通信必须通过内存来完成,这就是JMM。
2.2 Volatile的保证可见性
知道了JMM线程模型之后,我们就可以知道,主内存的变量,都是要被拷贝到工作线程的工作空间后,再进行操作。如果此时主内存理得变量发生变化,工作线程是感知不到的,我们可以用代码来进行示范:
public class MyData {
int number = 0;
public void addTo60(){
this.number = 60;
}
public void add(){
this.number++;
}
我们先定义一个类,MyData,只有一个普通int变量。
再定义一个测试类:
public class VisibleTest {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "comm in");
myData.number = myData.number ++;
System.out.println("myData"+myData.number);
}).start();
while(myData.number==0){
System.out.println(myData.number);
}
System.out.println(Thread.currentThread().getName()+"over");
}
}
此时我们通常会认为,number随着变量值的修改,会停止while循环的操作,但是结果并不会。
这是因为变量在拷贝回线程,修改值后并没有返回主存,导致另外一个线程感知不到,现在我们加上Volatile关键字:
volatile int number = 0;
线程顺利结束了!
这是因为加上volatile后,每次线程都要去主存来读取值,每次写完值后都要放回主存!
我们查看字节码文件:
其中 getfield 和putfield 都是从主存中获取和修改值。
但是volatile只保证读和写的和见性,并没有保证写的排他性,所以也就没有保证原子性。
2.2.1 内存屏障指令
volatile之所以能保证可见性和防止重排序,是因为他的底层是内存屏障指令:
内存屏障,是一个cpu指令,作用有两个:
一是保证特定操作的执行顺序
二是保证某些变量的内存可见性
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条memory barrier则会告诉编译器和cpu
,不管什么指令都不能和这条指令重排序,也就是说,通过内存屏障指令禁止在内存屏障前后的指令执行重新排序优化
内存屏障指令另一个作用是强制刷出各种cpu的缓存数据,因此任何cpu上的线程都能读取到这些数据的最新版本
2.3 Volatile禁止指令重排序
关于指令重排,我们先来举一个例子:
int x = 11; //1
int y = 15; //2
x = x + 5; //3
y = x * x; //4
这是四条简单的代码,我们都知道最后x = 16,y = 16 * 16
如果我们不知道指令重排序的话,可能只是简单地以为,语句的执行顺序为1234而已。
其实,计算机在执行程序的时候,为了提高性能,编译器和处理器常常会对指令进行重排,一般有以下三种:
源代码-> 编译器优化的重排->指令并行的重排->内存系统的重排->最终执行的指令
单线程环境里确保程序最终执行结果和代码执行的结果一致:也就是说,单线程环境无论如何重排指令,最终的结果都是一致的。
处理器在进行重排序时必须考虑指令之间的数据依赖性:也就是说,上述命令,4一定在3后面,1一定在3前面,不然就无法保证最终一致性了。
好的,单线程环境下没什么问题,接下来我们看多线程环境下:
两个线程同时运行时吗,就会出现这种结果,也可能会出现下面这种结果:
这样最终的结果就会不确定了,而volatile关键字就避免了指令重排序,按照编码的顺序来进行编译执行!
3.用volitile改写单例模式
通常我们认为的单例模式,在单线程下,都是这么写的
public class SingletomDeomo {
private static volatile SingletomDeomo instance = null;
private SingletomDeomo() {
System.out.println(Thread.currentThread().getName() + "\t 构造方法singletom");
}
public static SingletomDeomo getInstance(){
if(instance ==null){
instance = new SingletomDeomo();
}
return instance;
}
}
但是在多线程环境下,运行起来就会产生多个实例,那是因为由于上下文的切换,很多线程在判断完if为空后,时间片被别的线程夺去,然后别的线程又new了instance,导致现在的线程夺回时间片后,又会继续new一个对象。
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
SingletomDeomo.getInstance();
}).start();
}
}
为了解决这个问题,我们可以在这个方法上加上synchronize关键字,但是这样运行起来太重,我们可以使用dcl(double check lock) 双重校验锁来解决。
if (instance == null) {
synchronized (SingletomDeomo.class) {
if (instance == null) {
instance = new SingletomDeomo();
}
}
}
此时,如果instance 不加volatile关键字,还是会有问题。
我们要仔细分析一下instance = new SingletomDemo();里的步骤:
- 1.memory = allocate();
- 2.instance(memory);
- 3.instance = memory;
大概就是这么三步:
1.分配一块内存区域
2.将这块内存区域初始化
3.将这块内存区域分配给instance
注意:此时,这三步也是可以被重排序的!
所以,在instance上加上volatile就没事了!
以上便是对volatile的学习笔记。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。