synchronized关键字是阻塞式同步,在线程竞争激烈的时候会逐渐由偏向锁膨胀为重量级锁。而volatile是JVM提供的最轻量级的同步机制。JMM告诉我们各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。不过线程在工作内存中进行操作后将会何时写入主内存中?这个时机普通机制是没有规定的。
volatile一般用于修饰会被不同线程访问和修改的变量,而针对volatile修饰的变量给JVM给了规定: 线程对volatile变量的修改会立刻被其他线程感知,即被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,这样就不会出现数据脏读的现象,保证了数据的可见性 。
volatile具有可见性和有序性
加入volatile关键字的代码的class字节码中会多出了一个lock前缀指令,lock指令相当于一个内存屏障,主要做了三件事:
经过这一波操作后,其他的线程发现自己工作内存中的缓存失效后,就会从内存中重新读取该变量数据,即保证了其他线程可以获取当前最新值。即可以说volatile实现了缓存一致性协议: 每个处理器通过嗅探在总线上传播的数据来检查自己的缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
在之前JMM一文中对happens-before规则介绍,有一条是: 对一个volatile域的写,happens-before于任意后续对这个volatile域的读 。
来看一段代码
class VolatileExam{ private int a = 0; private volatile boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } public void reader() { if (flag) { //3 int i = a; //4 } } } 复制代码
对volatile的happens-before分析:
2 happens-before 3 1 happens-before 4 A happens-before B
volatile的内存语义分析:
为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序就得添加内存屏障。
四个内存屏障:
屏障类型 | 指令类型 | 说明 |
---|---|---|
LoadLoadBarriers | Load1;LoadLoad;Load2 | 确保Load1的数据的装载先于Load2及所有后续装载指令的装载 |
StoreStoreBarriers | Store1;StoreStore;Store2 | 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储 |
LoadStoreBarriers | Load1;LoadStore;Store2 | 确保Load1的数据的装载先于Store2及所有后续存储指令的存储 |
StoreLoadBarriers | Store1;StoreLoad;Load2 | 确保Store1的数据对其他处理器可见(刷新到内存)先于Load2及所有后续的装载指令的装载 |
为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序:
StoreStore屏障:禁止上面的普通写和下面的volatile写重排序; StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序 LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序 LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
复习一下JMM的8种原子操作:
volatile规定: read、load、use动作必须连续出现 ; assign、store、write动作必须连续出现 所以volatile保证: 每次读取前必须先从主内存刷新最新的值,每次写入后必须立即同步回主内存当中 。即volatile关键字修饰的变量看到的随时是自己的最新值。
public class VolatileExample { private static volatile int counter = 0; public static void main(String[] args) { //开十个线程,让他们每个都自增10000次,理论上应该得到10000 for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) counter++; } }); thread.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } } 复制代码
多次运行看出,每次都得不到10000,这说明volatile并不能保证整体原子性。问题就是counter++并不是一个原子操作,他包含了三个步骤:
这么看来如果线程1读取counter到工作内存后,其他线程对这个值已经做了自增操作,那么线程A的这个值自然就是一个过期的值,造成了数据的脏读,因此结果必然小于10000。
如果想让volatile保证整体原子性,必须符合:
如果编译器经过分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当做一个普通的变量来对待。