volatile的作用
1、保证线程可见性
-MESI
-缓存一致性协议
2、禁止指令重排序
-DCL单例
-Double Check Lock
-Mgr06.java
一、线程可见性
线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程间数据不可见的本质原因。
对volatile变量的写操作与普通变量的主要区别有两点:
1、修改volatile变量时会强制将修改后的值刷新的主内存中。
2、修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。
我们用代码写个例子来看看volatile的作用:
上面的代码按照逻辑上应该是没有问题的,先是启动线程t1,输出“m start… ”,然后main函数中线程sleep1秒后把t.running 设置为false,此时应当输出"m end!"。我们看一下输出结果:
程序一直在运行,一直都没有结束,也没有输出"m end!”。
我们加上volatile关键字试试:
import java.util.concurrent.TimeUnit;
public class VolatileDemo1 {
volatile boolean running=true;
void m(){
System.out.println("m start...");
while(running) {
}
System.out.println("m end! ");
}
public static void main(String[] args) {
VolatileDemo1 t=new VolatileDemo1();
new Thread(t::m ,"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running=false;
}
}
输出结果:
停了一秒后便输出"m end!",看来volatiel还是有作用的~~~~~~~
我们来分析一下为什么???
每一个线程都有一份自己的本地内存,所有线程共用一份主内存。如果一个线程对主内存中的数据进行了修改,而此时另外一个线程不知道是否已经发生了修改,就说此时是不可见的。
在上面的没有用volatie的代码中,mian线程的running改了,主内存的数据也改了,可是t1线程的runnig不知道还没有知道发生了改变,所以t1的running还没有改变,running一直是true,所以程序一直不结束。
volatile关键字的作用很简单,就是一个线程在对主内存的某一份数据进行更改时,改完之后会立刻刷新到主内存。并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。这样一来就保证了可见性。
所以后面使用了volatile,程序就可以正常输出"m end!"并结束。
二、禁止指令重排序
我们用单例模式的双重校验来看一下Volatile的禁止指令重排序的作用。
这样的代码一般运行是不会出错的,可是在高并发的时候(例如京东抢拍)有可能会出错。
双重校验的写法:第一次判断是否为null是为了拒绝掉当对象不为空的时候剩余的线程。里面加锁是为了当对象为null的时候,此时同时进来两个线程(A和B两个线程),我们要保证只有一个线程才可以初始化对象,所以在这里面加上了锁,这样A拿到了锁进去初始化对象,然后进行返回,B再进去此时发现不为null,那么就不执行初始化的过程。这样就能保证上面的单例模式的正常运行,同时为系统也是节约了许多开销(避免每个线程进来加锁–懒汉式写法等。。)
我们来分析一段代码:
INSTANCE = new VolatileDemo2();
我们首先要理解对象实例化的步骤:
1、分配内存空间。
2、初始化对象。
3、将内存空间的地址赋值给对应的引用。
以上是一般正常的步骤,如果在高并发的时候,有可能2和3 的顺序会颠倒,导致还没有初始化,就把默认值直接赋值了。如果发生这种情况,那么此时拿到的对象也只是一个引用,对于后面的业务操作可能存在错误的发生。
为了防止这种情况发生,我们要加上volatile关键字:
public class VolatileDemo2 {
private static volatile VolatileDemo2 INSTANCE;
private VolatileDemo2(){}
public static VolatileDemo2 getInstance(){
if(INSTANCE == null){
synchronized (VolatileDemo2.class){
if(INSTANCE == null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new VolatileDemo2();
}
}
}
return INSTANCE;
}
public static void main(String[] args){
for(int i=0;i<100;i++){
new Thread(()->{
System.out.println(VolatileDemo2.getInstance().hashCode());
}).start();
}
}
}
输出结果也是一样的,只是保证在高并发时也不会出错。
再举个栗子
public class VolatileTest {
public volatile static boolean shareFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.print("开始执行线程1 =>");
while (!shareFlag){ //shareFlag = false则一直死循环
//System.out.println("shareFlag=" + shareFlag);
}
System.out.print("线程1执行完成 =>");
}).start();
Thread.sleep(2000);
new Thread(() -> {
System.out.print("开始执行线程2 =>");
shareFlag = true;
System.out.print("线程2执行完成 =>");
}).start();
}
}
输出:开始执行线程1 =>开始执行线程2 =>线程2执行完成 =>线程1执行完成
简单的说就是
- 当线程2修改shareFlag的时候(参考Modify),告知bus总线我修改了共享变量shareFlag,
- 线程1对Bus总线进行监听,当它获知共享变量shareFlag发生了修改就会将自己工作内存中的shareFlag副本删除使其失效。
- 当线程1再次需要使用到shareFlag的时候,发现工作内存中没有shareFlag变量副本,就会重新从主内存中加载(read&load)
参考文章:JAVA中volatile介绍
你应该要理解的java并发关键字volatile
Java并发-volatile与JMM多线程内存模型