1.volatile简介
/** * @author Lin * @Date 2018/2/22. */ public class TestVolatile { private static boolean isOver = false; public static void main(String[] args) { new Thread( () -> {while (!isOver){ System.out.println("111"); }} ).start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } isOver = true; System.out.println("End"); } }
先看一段代码,启动一个线程,由于isOver = false,所以该线程中run方法的while是死循环。企图在main方法中更改isOver的值去终止线程,但是实时上该程序的线程并没有被终止,始终陷入死循环中,线程并没有终止退出。
首先我们对volatile的基本认知是“被volatile修饰的变量对每个线程是可见的,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,从而避免数据脏读”。
2.深入理解volatile关键字
- volatile保证可见性
当变量被volatile修饰时会具备两种语意:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
解释下上面代码中线程不能被终止,熟悉java内存相关知识的都知道每个线程在运行时都有自己的工作内存,那么上面线程运行是会将isOver变量拷贝一份到自己的工作内存中,当main线程去修改isOver的 值时,并不会影响到自定义线程工作内存中的isOver的值。
但是使用volatile修饰就不一样了:
1)使用volatile关键字会强制将修改的值立即写入主存
2)使用volatile之后当main线程修改isOver后会导致自定义线程工作内存中的isOver无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效),所以自定义线程再次读取变量isOver的值时会 去主存读取。
2)禁止进行指令重排序 - volatile不确保原子性(此处要注意,不多记录)
3.volatile的实现原理
可见性:
在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令。我们想这个Lock指令肯定有神奇的地方,那么Lock前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影 响:
- 将当前处理器缓存行的数据写回系统内存;
- 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向 处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处 理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成 无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论:
- Lock前缀的指令会引起处理器缓存写回内存;
- 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
- 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
有序性:
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它 前面的操作已经全部完成。
理解了volatile就处理上面代码的问题了。
/** * @author Lin * @Date 2018/2/22. */ public class TestVolatile { private static volatile boolean isOver = false; public static void main(String[] args) { new Thread( () -> {while (!isOver){ System.out.println("111"); }} ).start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } isOver = true; System.out.println("End"); } }