Java基础教程:多线程杂谈——Volatile
引入Volatile
Java语言提供了一种稍弱的同步机制,即Volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为Volatile类型后,编译器与运行时都会注意到整个变量是共享的,因此不会将该变量上的操作和去其他内存操作一起重排序。Volatile变量不会被缓存在寄存器或者对其它处理器不可见的地方,因此在读取Volatile类型的变量时总会返回最新写入的值。
实现原理
在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令。我们想这个Lock指令肯定有神奇的地方,那么Lock前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影响:
- 将当前处理器缓存行的数据写回系统内存;
- 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论:
- Lock前缀的指令会引起处理器缓存写回内存;
- 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
- 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。
无法保证原子性
public class VolatileExample { private static volatile int counter = 0; public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) counter++; } }); thread.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); }}
使用场景
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只可以确保可见性。
当且仅当满足以下所有条件的时,才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
状态标记
volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);
参考资料