volatile 作为平时使用不多,但是面试时经常问的一个方面,作为java开发人员,不理解和说清楚这个东西的原理和机制,都不好意思找工作,那么怎么理解这个东西呢。
说起这个关键字的性质,相信都是随口就来,voliatile能保证可见性和一致性,那么怎么理解呢,要理解这两点需要知道JMM
1. java内存模型基础
java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。可以避免像c++等直接使用物理硬件和操作系统的内存模型在不同操作系统和硬件平台下表现不同,比如有些c/c++程序可能在windows平台运行正常,而在linux平台却运行有问题,java虚拟机规范定义java内存模型屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
java内存模型规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
用一段话来说就是java虚拟机规定了这样一种模型,来保证多线程读写的同步问题,这个模型有这样的特点:1.所有的变量都在主存中分配;2.每个线程都有自己的工作内存;3.线程对变量的修改或者使用都必须现将变量从主存读到工作内存中使用;这个模型就是JMM。
使用一个书上的图
JMM规定了普通变量8种基础内存操作
lock、unlock、read、load、use、assign(赋值)、store、write;
lock 作用于主存中的变量,用于表示该变量已被其他线程锁定;
unlock 作用于主存的变量,用于解锁;
read 作用于主存中的变量,将主存中的变量加载到工作内存中;
load 作用于工作内存的变量,将工作内存的变量赋值成从主存read过来的变量;
use 作用于工作内存的变量,是线程读取工作内存变量,用于操作数栈的入栈操作;
assign 作用于工作内存的变量,线程将操作数中的运算结果复制给变量;
store 作用于工作内存的变量, 将工作内存的变量加载到主存中;
write 作用于主存中的变量,将主存的变量赋值成从主存store的值;
对于这8中基础操作有8中基本规则:
1.lock必须和unlock一起出现;
2.read和load必须一起出现;
3.write和store必须一起出现;
4.use或者assign之前必有load;
5.assign之后必有store;
6.lock unlock,read load ,store write 三个顺序不能反
7.unlock之前一定有write
8.loack之后一定有load
validate比普通变量多的操作规定
1.use之前的动作一定是load (可以理解为时间上直接发生,不做其他处理)
这个动作保证了线程每次使用的变量都是主存中的最新的变量
2.assign之后的动作一定是store之后是write
这个动作保证了线程每次更新变量的值都会直接同步到主存中
3 如果线程1的use/assign操作在线程2的use/assign操作之前,那么线程1的read/write一定在线程2的read/write之前
这个动作保证了线程的一致性,增加了内部屏障,禁止指令重排序
规定1和规定2使得validate修饰的变量变量具有可见性,即一个线程修改之后会立马被其他线程观察到。根据这一特性平时使用validate可以用来状态标志 独立观察 一次性安全发布
状态标志
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested 标志从 false 转换为 true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能扩展(从 false 到 true,再转换到 false)。此外,还需要某些原子状态转换机制,例如原子变量
一次性安全发布
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;
public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}
public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}
如果 theFlooble 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会得到一个不完全构造的 Flooble。
该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布之后永远不会被修改)。volatile 类型的引用可以确保对象的发布形式的可见性,但是如果对象的状态在发布后将发生更改,那么就需要额外的同步。
独立观察
public class UserManager {
public volatile String lastUser;
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}
该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布后不会更改。使用该值的代码需要清楚该值可能随时发生变化。
第三个特征的用处
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance==null) { // 1
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton(); // 2
}
}
return instance;
}
}
这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给instance加上了volatile。
为什么用了synchronized还要用volatile?具体来说就是synchronized虽然保证了原子性,但却没有保证指令重排序的正确性,会出现A线程执行初始化,但可能因为构造函数里面的操作太多了,所以A线程的instance实例还没有造出来,但已经被赋值了(即代码中2操作,先分配内存空间后构建对象)。
而B线程这时过来了(代码1操作,发现instance不为null),错以为instance已经被实例化出来,一用才发现instance尚未被初始化。要知道我们的线程虽然可以保证原子性,但程序可能是在多核CPU上执行。