对于java开发工程师来讲,并发编程一直是一个具备挑战性的技术,本章将给你们介绍一下volatile的原理。spa
下面介绍几个概念:线程
共享变量:共享变量是指能够同时被多个线程访问的变量,共享变量是被存放在堆里面,全部的方法内临时变量都不是共享变量。code
重排序:重排序是指为了提升指令运行的性能,在编译时或者运行时对指令执行顺序进行调整的机制。重排序分为编译重排序和运行时重排序。编译重排序是指编译器在编译源代码的时候就对代码执行顺序进行分析,在遵循as-if-serial的原则前提下对源码的执行顺序进行调整。as-if-serial原则是指在单线程环境下,不管怎么重排序,代码的执行结果都是肯定的。运行时重排序是指为了提升执行的运行速度,系统对机器的执行指令的执行顺序进行调整。
可见性:内存的可见性是指线程之间的可见性,一个线程的修改状态对另一个线程是可见的,用通俗的话说,就是假如一个线程A修改一个共享变量flag以后,则线程B去读取,必定能读取到最新修改的flag。
说到这里,可能有些同窗会以为,这不是废话吗,线程A修改变量flag后,线程B确定是能够拿到最新的值的呀。假如你真的这么认为,那么请运行一下如下的代码:
package test; public class VariableTest { public static boolean flag = false; public static void main(String[] args) throws InterruptedException { ThreadA threadA = new ThreadA(); ThreadB threadB = new ThreadB(); new Thread(threadA, "threadA").start(); Thread.sleep(1000l);//为了保证threadA比threadB先启动,sleep一下 new Thread(threadB, "threadB").start(); } static class ThreadA extends Thread { public void run() { while (true) { if (flag) { System.out.println(Thread.currentThread().getName() + " : flag is " + flag); break; } } } } static class ThreadB extends Thread { public void run() { flag = true; System.out.println(Thread.currentThread().getName() + " : flag is " + flag); } } }
运行结果:
以上运行结果证实:线程B修改变量flag以后,线程A读取不到,A线程一直在运行,没法中止。
内存不可见的两个缘由:
一、cache机制致使内存不可见
咱们都知道,CPU的运行速度是远远高于内存的读写速度的,为了避免让cpu为了等待读写内存数据,现代cpu和内存之间都存在一个高速缓存cache(其实是一个多级寄存器),以下图:
线程在运行的过程当中会把主内存的数据拷贝一份到线程内部cache中,也就是working memory。这个时候多个线程访问同一个变量,其实就是访问本身的内部cache。
上面例子出现问题的缘由在于:线程A把变量flag加载到本身的内部缓存cache中,线程B修改变量flag后,即便从新写入主内存,可是线程A不会从新从主内存加载变量flag,看到的仍是本身cache中的变量flag。因此线程A是读取不到线程B更新后的值。
二、除了cache的缘由,重排序后的指令在多线程执行时也有可能致使内存不可见,因为指令顺序的调整,线程A读取某个变量的时候线程B可能尚未进行写入操做呢,虽然代码顺序上写操做是在前面的。
volatile的原理:
volatile修饰的变量不容许线程内部cache缓存和重排序,线程读取数据的时候直接读写内存,同时volatile不会对变量加锁,所以性能会比synchronized好。另外还有一个说法是使用volatile的变量依然会被读到cache中,只不过当B线程修改了flag以后,会将flag写回主内存,同时会经过信号机制通知到A线程去同步内存中flag的值。我更倾向于后者的解释,还望大神指导一下正确的答案。
可是须要注意的是,volatile不保证操做的原子性,请勿使用volatile来进行原子性操做。