首先volatile有两大功能:
保证线程可见性
禁止指令重排序
1、保证线程可见性
首先我们来看这样一个程序,其中不加volatile关键字运行的结果截然不同,加上volatile程序能够正常结束,不加则程序进入死循环;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
package com.designmodal.design.juc01;
import java.util.concurrent.TimeUnit;
/**
* @author D-L
* @Classname T001_volatile
* @Version 1.0
* @Description volatile 保证线程的可见性
* @Date 2020/7/19 17:30
*/
public class T001_volatile {
//定义一个变量running
volatile boolean running = true ;
public void m(){
while (running){
//TODO 不做任何的处理
System.out.println( "while is running When can I stop -------------" );
}
System.out.println( "method is end ---------------" );
}
public static void main(String[] args) {
T001_volatile t001_volatile = new T001_volatile();
new Thread(t001_volatile::m , "Thread t1" ).start();
//停一秒
try {
TimeUnit.SECONDS.sleep( 1 );
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改running的值
t001_volatile.running = false ;
}
}
|
通过上面的小程序说明volatile是具有保证线程之间的可见性的功能的,具体是如何实现的呢?下面给大家解释一下:
之前在上一篇讲synchronized时提到了 堆内存是线程共享的,而线程在工作时有自己的工作内存,对于共享变量running来说,线程1和线程2在运行的时候先把running变量copy到自己工作内存,对这个变量的改变都是在自己的工作内存中,并不会直接的反映到其他线程,如果加了volatile,running变量改变其他线程很快就会知道,这就是线程的可见性;
这里用到的是:MESI(CPU缓存一致性协议) MESI的主要思想:当CPU写数据时,如果该变量是共享数据,给其他CPU发送信号,使得其他的CPU中的该变量的缓存行无效;归根结底这里需要借助硬件来帮助我们。
volatile保证线程可见性但是不能代替synchronized:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
package com.designmodal.design.juc01;
import java.util.ArrayList;
import java.util.List;
/**
* @author D-L
* @Classname VolatileAndSynchronized
* @Version 1.0
* @Description synchronized can not be replaced by volatile
* volatile 不能代替synchronized
* 只能保证可见性 不能保证原子性
* count++ 不是原子性操作
* @Date 2020/xx/xx 23:25
*/
public class VolatileAndSynchronized {
volatile int count = 0 ;
public synchronized void m(){
for ( int i = 0 ; i < 1000 ; i++) {
//非原子性操作 汇编指令至少有三条
count++;
}
}
public static void main(String[] args) {
VolatileAndSynchronized v = new VolatileAndSynchronized();
List<Thread> threads = new ArrayList<>();
for ( int i = 0 ; i < 10 ; i++) {
threads.add( new Thread(v::m , "Thread" + i));
}
threads.forEach(o ->o.start());
threads.forEach(o ->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(v.count);
}
}
|
2、禁止指令重排序
指令重排序也是和CPU有关系,加了volatile之后,每次写都会背线程看到。CPU原来执行指令时,是按照一步一步顺序来执行的,但是CPU为了提高效率它会把指令并发来执行,第一个指令执行到一半的时候第二条指令就可能已经开始执行了,这叫流水线式的执行;为了充分的利用CPU,就要求编译器把编译完的源码指令,可能会进行一个指令重新排序;这种架构通过实际验证,很大效率上提高了CPU的使用效率。
下面从一个面试题来讨论一下指令重排序:
面试官:你听过单例模式吗?
你:当然听过,不然没法聊了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
package com.designmodal.design.juc01;
import java.util.concurrent.TimeUnit;
/**
* @author D-L
* @Classname T002_volatile
* @Version 1.0
* @Description volatile 指令重排序
* @Date 2020/7/20 00:48
*/
public class T002_volatile {
//创建私有的 T002_volatile 有人会问这里的volatile要不要使用,这里的答案是肯定的
private static /**volatile*/ volatile T002_volatile INSTANCE;
public T002_volatile() {}
public T002_volatile getInstance(){
//模拟业务代码 这里为了synchronized更加细粒度,所以使用了双重检查
if (INSTANCE == null ){
synchronized ( this ){
//双重检查
if (INSTANCE == null ){
//避免线程之间的干扰 在这里睡一秒
try {
TimeUnit.SECONDS.sleep( 1 );
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建实例对象
INSTANCE = new T002_volatile();
}
}
}
return INSTANCE;
}
/**
* 创建100个线程 调用getInstance() 打印hashcode值
* @param args
*/
public static void main(String[] args) {
T002_volatile t001_volatile = new T002_volatile();
for ( int i = 0 ; i < 100 ; i++) {
new Thread(() ->{
T002_volatile instance = t001_volatile.getInstance();
System.out.println(instance.hashCode());
}).start();
}
}
}
|
在上述的代码中:INSTANCE = new T002_volatile(); 经过编译后的指令是分三步的
1、给指令申请内存
2、给成员变量初始化
3、把这块对象的内容赋给INSTANCE
在第二步这里既然已经有默认值了,第二个线程来检查,发现已经有值了根本就不会进入锁住的那份代码;加了volatile就不会出现指令重排序了,所以在这个时候一定要保证初始化完成之后才会赋值给这个变量,这就是volatile存在的意义。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://www.cnblogs.com/dongl961230/p/13342688.html