保证了不同线程对共享变量操作的可见性, 即一个线程修改了某个共享变量的值, 那么这个新值对其他线程是可见的.
// thread 01
boolean stop = false;
while(!stop) {
doSomething()
}
// thread 02
stop = true;
上面的代码, 当线程2修改了stop的值, 但是还没来得及写入主存, 那么线程1就会一直在循环中调用doSomething方法.
那么当用volatile修饰stop变量后:
禁止进行指令重排序
当程序执行到volatile变量的读或写操作时, 在前面的操作的更改肯定是已经进行, 并且结果对后面的操作可见; 在其后面的操作肯定还没有进行.
在进行指令优化时, 不能将对volatile变量访问的语句放在后面执行, 也不能把volatile后面的语句放到前面执行.
举个例子:
// thread 01
context = loadContext();
inited = true;
// thread 02
while(!inited) {
sleep();
}
doSomething();
由于线程1中的语句1和语句2没有数据依赖关系, 因此有可能会被重排, 那么在context没有读取完毕的情况下, 线程2就有可能做下一步的操作.
由此可见, 指令重排序不会影响单个线程的执行, 但是会影响到线程并发性的正确性.
那么我们用volatile修饰inited后就不会出现这个问题了. 因为这样在编译期间, 会在指令序列中插入内存屏障来禁止特定类型的处理器重排序.
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令. <深入理解Java虚拟机>
LOCK指令前缀会设置处理器的LOCK#信号, 这个信号会使总线锁定, 阻止其他处理器接管总线的访问, 使得指令的执行变成原子操作, 完成处理器对共享内存的独享使用.
LOCK指令前缀实际上相当于一个内存屏障, 会提供3个功能:
应用前提:
应用场景:
状态标记量
// thread 01
context = loadContext();
volatile boolean inited = true;
// thread 02
while(!inited) {
sleep();
}
doSomething();
double check
class Singleton {
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这样可以减少每次获取单例是都要获得类锁的开销, 但是需要对引用用volatile修饰, 因为在Java中不同步的情况下引用不是线程安全的, 所以如果不用volatile修饰, double check是无效的.
其实这里更推荐Initialization on Demand Holder的方法来实现单例模式:
class Singleton {
static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getSingleton() {
return SingletonHolder.instance;
}
}
我们知道, 在Java中初始化一个类, 包括执行这个类的静态初始化和在这个类中的静态字段. 当发生下列任意情况后, 类会被初始化: