使用 volatile
修饰的成员变量具有两个特性,分别是 「可见性」 和 「有序性」。
可见性
使用 volatile
修饰的成员变量,其发生的改变对其他所有线程立即可见,而普通变量是做不到这一点的。也就是说 volatile
修饰的变量只要发生了改变,其他线程总是在使用这个变量的时候会获取到最新的值。而对普通变量来说,一个线程的修改对其他线程是不可见的,其他线程获取到的可能还是旧的值。
要完全理解上面这段话,就需要先了解 Java内存模型(JMM,Java Memory Model)以及 缓存一致性问题。
Java 内存模型
Java 内存模型规定所有的共享变量都是存储在主内存的。然后每个线程都会有自己的一个工作内存,用于缓存该线程使用到的存放于主内存的共享变量的副本。线程对变量的操作都是在工作内存中完成的,然后再写回主内存。
这样设计的原因是直接操作主内存是IO操作比较耗时,所以增加了工作内存(CPU 高速缓存)将需要的数据一次性读取并缓存。计算完毕后再同步(写)回到主内存。
缓存一致性问题
加入工作内存后确实提高了与主内存交互的速度,但也引入了一个新的问题:缓存一致性。
当两个线程都从主内存读取了变量x
,并缓存到各自的工作内存当中。这个时候如果其中一个线程2
修改了变量的值,那么就会造成线程1
在工作内存中缓存的数据错误。
我们当然可以对共享变量加锁来避免这个问题,但比较重。而 volatile
就是解决缓存一致性问题最轻量级的实现。那么volatile
是如何实现的呢?这就要从 volatile
的特殊规则说起。
volatile的特殊规则
Java 内存模型规定:
-
被 volatile
修饰的变量只要发生改变,就必须立马回写到主内存。 -
只要使用到被 volatile
修饰的变量,就必须从主内存读取最新的值。
以上2
点就保证了只要线程中用到被 volatile
修饰的变量,其值肯定是最新的。
线程同步
volatile
可见性特性的一个用途是可以使用其在多个线程之间做信号同步。
如下面伪代码所示,如果 shutdown
没有被申明为 volatile
,那么当某个线程将 shutdown
标记为 true
之后,其他线程可能并不会立即停止执行 dosomething
方法,因为其他线程的工作内存中 shutdown
可能还是 false
。
private volatile boolean shutdown = false;
// 多线程调用
public void dosomething(){
while(!shutdown){
// 做一些事情
}
}
public void setShutdown(){
shutdown = true;
}
禁止指令重排
volatile
的第二个特性是「禁止指令重排优化」。
先来说说什么是指令重排,JVM 为了优化程序运行速度,在不影响 单线程 最终结果的条件下,可能会对运行指令进行重新排序。
比如下面这段代码,第一行和第二行就可能被重新排序,重排后对执行结果没有影响。
// 重排前
int a = 1;
int b = 2;
int c = a + b;
// 重排后
int b = 2;
int a = 1;
int c = a + b;
指令重排带来的影响
在多线程执行的情况下,指令重排就会带来不可预测的风险。
比如下面这段伪代码:线程1
通过IO加载配置信息,加载完毕后将完成标记 initialized
设置为 true
,且当 initialized==true
的时候,这个类才能正常使用。
private initialized = false;
// 线程1
public void init(){
config = loadConfig()
initialized = true
}
// 线程2
public void dosomething(){
if(initialized){
// 做一些事情。。。
}
}
一旦发生指令重排,将 initialized
赋值为 true
的指令排到加载配置信息的前面去了,那么线程2
就可能会出问题。因为虽然线程2
中得到 initialized
等于 true
,但是此时由线程1
加载的配置信息可能还没有完成。
private initialized = false;
// 线程1
public void init(){
initialized = true
config = loadConfig()
}
// 线程2
public void dosomething(){
if(initialized){
// 做一些事情。。。
}
}
上述指令重排在同一个线程下是没有任何问题的。因为同一个线程的话只有将 init
方法执行完毕之后,才能执行后续其他方法。所以不管 initialized
的赋值语句被重排到哪里,只要 init
方法执行完了,配置信息肯定也就加载完成了。
使用volatile禁止指令重排
还是上面那段代码,我们只要将 initialized
前面加上 volatile
修饰符,就可以禁止指令重排。
private volatile initialized = false;
// 线程1
public void init(){
config = loadConfig()
initialized = true
}
// 线程2
public void dosomething(){
if(initialized){
// 做一些事情。。。
}
}
这样线程2
只要判断出 initialized
为 true
,那么配置信息就一定是加载完成了的,就不会出现问题了。
双重检查单例模式
禁止指令重排的另一个典型使用就是 双重检查单例模式。
package info.iyushu.design.pattern.singleton;
public class LazySingletonInstance {
// 保证 getInstance 返回的是初始化完全的对象。
private static volatile LazySingletonInstance instance;
// 私有化构造器
private LazySingletonInstance(){
System.out.println("LazySingletonInstance 构造函数。。。");
}
public static LazySingletonInstance getInstance(){
System.out.println("获取 SingletonInstance 实例");
if(instance == null){
synchronized (LazySingletonInstance.class){
if(instance == null){
instance = new LazySingletonInstance();
}
}
}
return instance;
}
}
上述代码中,因为 new LazySingletonInstance()
这个操作不是原子性的,如果没有用 volatile
修饰,在多线程下有可能获取到的对象还没有初始化完成。
– End –
原文始发于微信公众号(i余数):【Java面试】聊聊 volatile 关键字
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/194053.html