什么是可见性?
可见性就是,多线程环境中,对共享变量的修改对于其他线程是否立即可见的问题。
举个例子:
- 子线程,1s之后,将flag修改为true
public class TestVolatile extends Thread{
boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=true;
System.out.println("flag:"+isFlag());
}
public boolean isFlag() {
return flag;
}
}
- 主线程,不断判断flag的值,如果被修改,则打印并终止
public class Main {
public static void main(String[] args) throws InterruptedException {
TestVolatile thread = new TestVolatile();
thread.start();
while (true) {
if (thread.isFlag()) {
System.out.println("flag被改了");
break;
}
}
}
}
如果一切正常的话,1s后,主线程将会打印"flag被改了",但是真实情况确是子线程真的把flag改了,但是主线程一直没有感知到flag的变化,进入了死循环。这就是没有满足可见性所带来的问题。
为什么多线程之间存在不可见的问题?
要回答这个问题,就需要来看看Java内存模型:
Java内存模型
所有变量都被定义在主内存中,每个线程有自己的工作内存,线程需要先将主存中的变量拷贝到工作内存中,操作完成后,再同步到主内存。为什么会有工作内存呢?因为工作内存中的变量会放入CPU的高速缓存中,而主内存的变量就在物理内存中,这样可以提高速度。
对上面的例子做一个分析:
- 子线程启动后,主存变量flag=false
- 主线程进入while(true)循环,从主存中拷贝flag到主线程的工作内存,因为flag为false,所以一直循环
- 1s后,子线程从主存拷贝flag到子线程的工作内存,并对其修改,但并没有立即将该修改同步到主存
- 主线程工作内存中的值一直是false,将会一直循环下去
但是这里有一个问题,子线程是没有立即将flag同步到主存,但迟早会同步过去啊,那么主线程总有某一时刻会发现主存的flag被改了,然后从主存刷新flag的新值到工作内存,while循环也会停止才对啊,为啥就停不下来了呢?
问题的关键就在while(true)中,根据尚硅谷的NIO教程,while(true)是一个执行效率极高的操作,它的速度太快了,根本不给从主存刷新值的机会,所以才会停不下来。我们可以给while(true)代码加入Thread.sleep(1);,这时候,会发现主线程可以从主存刷新值了,代码可以正常结束了。可是问题是,谁写代码老加个sleep(),另外,我主线程当然想第一时间就感知到flag的修改,为啥让我休眠1ms。
这时候,就需要volatile闪亮登场了,我们给flag加上volatile修饰,再次运行,发现即使不sleep(),主线程也能感知到flag的修改了。
为啥volatile能保证可见性?
volatile规则:修改了volatile变量,必须立即同步到主存,同时使其他线程工作内存中的值变为无效;使用volatile变量前,必须先从主存刷新,以此来保证可见性
同样以上面的代码为例:子线程将flag修改为true,同步到主存,同时使主线程的工作内存中的flag失效,主线程下次使用flag时if (thread.isFlag())
,发现工作内存中的flag已经失效,而且由于volatile的影响,在使用flag前本来也会强制从主存刷新,将会得到最新的值true
有其他办法能保证可见性吗?
在参考的资料中均有提到,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性。
但是我给子线程的flag=true;这一行加上synchronized同步代码块儿或者可重入锁,发现都未能实现可见性啊,可能是因为它只保证了修改flag立即同步到主存,却没能让主线程工作空间中的flag失效,也没有要求在使用flag之前必须从主存刷新吧。
最佳实践
由此例可以看出,volatile对于单纯的set/get场景是非常好用的
网友评论