volatile可以保证可见性和有序性。
-
可见性:禁止了JVM的缓存优化
-
有序性:禁止了JVM的重排序
可见性
static boolean run = true;
public static void main(String[] args) {
Thread t = new Thread(()->{
while(run){
//循环体
}
});
t.start();
sleep(1000);
run = flag;
}
- 初始状态时t线程从主存中读取run的值到t线程的工作内存中
- 为了提高访问速率,JIT编译器会将run的值缓存到t线程工作内存中的高速缓存中。
- 1秒后,main线程修改了run的值并同步到主存中,但是t线程一直再缓存中读取这个变量,结果依然式旧的。
解决方法
volatile
修饰的变量被线程读取时,必须要到主存中获取他的值。这样可以保证一个线程对该变量的修改对其他线程是可见的。
synchronize也可以保证代码块中的变量的可见性
Balking模式+两阶段终止模式
balking:犹豫模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程无需再做了,直接结束返回。
应用实例:单例模式。多次点击开始按钮只启动一个监控线程。
两阶段终止:一个线程“优雅”的终止另一个线程,优雅:给被终止的线程料理后事的机会。
public class Balking {
private volatile boolean stop = false;
private boolean starting = false;
private Thread monitorThread;
public void start(){
synchronized (this){
if(starting) //保证监控线程只启动一次
return;
starting = true; //synchronized也可以保证可见性。
}
monitorThread = new Thread(()->{
while (true){
if(stop){
System.out.println("监控停止,处理善后工作");
}
System.out.println("2s监控一次");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
monitorThread.start();
}
public void stop(){
stop = true; //终止正常状态
monitorThread.interrupt(); //中止休眠状态
}
}
有序性
为什么要进行指令重排
现代cpu设计为一个时钟周期完成一条执行时间最长的cpu指令。每条指令可以细分为IF-ID-EX-MEM-WB
五个阶段。对于多级指令流水线可以同时执行这五个阶段,这时cpu可以在一个时钟周期内执行五条指令的不同阶段。提高了指令的吞吐率。
//可以重排的例子
int a = 10; //指令1
int b = 10; //指令2
//不可以重排的例子
int a = 10; //指令1
int b = a - 5;·//指令2
指令重排带来的问题
int num = 0;
boolean ready = false;
线程1执行此方法
public void fun1(Result r){
if(ready){
r.r1 = num + num;
}else{
r.r1 = 1;
}
}
//线程2执行此方法
public void fun2(Result r){
num = 2;
ready = true;
}
可能会出现意料之外的结果:r1=0。这是由于发生指令重排,线程2先执行完了ready = true
指令,这时轮到线程1执行,所以结果是num = 0
。
如何保证有序性
使用volatile
,底层原理是内存屏障。
- 写屏障确保指令重排时不会将写屏障之前的代码排在写屏障之后
public void fun1(Result r){
num = 2;
ready = true; //ready使用volatile修饰时num=2不会在此代码之后执行
}
- 读屏障之后的代码不会出现在屏障之前。
单例模式-volatile保证有序性的实践
class Singleton{
private Singleton(){}
//synchronized也可以保证同步块中的单例对象的可见性,所以此处的volatile是为了保证有序性而不是可见性(虽然也可以保证)
private volatile static Singleton INSTANCE = null;
public synchronized static Singleton getInstance(){
if(INSTANCE == null){ //如果其他线程已经创建了单例对象,就不需要进入同步代码块,保证了只有第一次创建时才会进入同步块。
synchronized (Singleton.class){
if(INSTANCE == null){ //在第一次创建单例对象时可能会有多个线程竞争,这次检测保证只创建一个单例对象
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
对于此处volatile保证有序性的解释:
INSTANCE = new Singleton();
,这句代码对应的字节码有以下4条
- 创建对象,将对象引用入栈 // new Singleton()
- 复制引用地址
- 调用构造方法
- 把引用赋值给INSTANCE。
也许jvm会优化为先执行4,在执行3。如果这时候t1还未完全将构造方法执行完,那么t2线程得到的是一个没有初始化完毕的单例。使用volatile可以避免这个问题。