volatile是变量修饰符,其修饰的变量具备内存可见性。java
可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会当即被更新到主存,当有其余线程须要读取时,能够当即获取修改以后的值。c++
在Java中为了加快程序的运行效率,对一些变量的操做一般是在该线程的寄存器或是CPU缓存上进行的,以后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。缓存
实例讲解:并发
class FlagThread extends Thread{ private boolean flag = false; @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag : " + flag); } public boolean isFlag() { return flag; } } public class TestVolatile { public static void main(String[] args) { FlagThread flagThread = new FlagThread(); flagThread.start(); while (true){ if (flagThread.isFlag()){ System.out.println("------------------------"); break; } } } }
程序输出:(main线程结束不了)ide
问题分析:性能
在操做共享数据的时候,系统会将共享数据放置在主存中。优化
流程分析:.net
步骤一:线程
线程1:读取到的主存数据,flag = true;code
main线程:读取到的主存数据,flag = true;
步骤二:
线程1:修改自身线程缓存空间的值,令flag = false;并将其更新到主存中,此时主存中的flag = false.
可是,main线程中的值,仍然是,flag = false,并一直循环下去。
所以,main线程始终在循环中,没法检测到falg已经变化的值。
volatile能够禁止进行指令重排。
指令重排是指处理器为了提升程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,可是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,可是会影响到线程并发执行的正确性。
程序执行到volatile修饰变量的读操做或者写操做时,在其前面的操做确定已经完成,且结果已经对后面的操做可见,在其后面的操做确定尚未进行。
volatile的使用举例
//线程1: context = loadContext(); //语句1 context初始化操做 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
由于指令重排序,有可能语句2会在语句1以前执行,可能致使context还没被初始化,而线程2中,
// 此处判断为false,则不会进入循环 while(!inited ){ sleep() }
于是,线程2使用未初始化的context去进行操做,致使程序出错。
这里若是用volatile关键字对inited变量进行修饰,就不会出现这种问题了,这是由于volatile禁止指令重排:程序执行到volatile修饰变量的读操做或者写操做时,在其前面的操做确定已经完成,且结果已经对后面的操做可见,在其后面的操做确定尚未进行。
synchronized可做用于一段代码或方法,既能够保证可见性,又可以保证原子性。
可见性体如今:经过synchronized或者Lock能保证同一时刻只有一个线程获取锁而后执行同步代码,而且在释放锁以前会将对变量的修改刷新到主存中。
原子性表如今:要么不执行,要么执行到底。
实例讲解:必须使用synchronized而不能使用volatile的场景
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
程序分析:
结果:例子中用new了10个线程,分别去调用1000次increase()方法,每次运行结果都不一致,都是一个小于10000的数字。
问题分析:自增操做不是原子操做,volatile 是不能保证原子性的。回到文章一开始的例子,使用volatile修饰int型变量i,多个线程同时进行i++操做。好比有两个线程A和B对volatile修饰的i进行i++操做,i的初始值是0,A线程执行i++时刚读取了i的值0,就切换到B线程了,B线程(从内存中)读取i的值也为0,而后就切换到A线程继续执行i++操做,完成后i就为1了,接着切换到B线程,由于以前已经读取过了,因此继续执行i++操做,最后的结果i就为1了。同理能够解释为何每次运行结果都是小于10000的数字。
可是使用synchronized对部分代码进行以下修改,就能保证同一时刻只有一个线程获取锁而后执行同步代码。运行结果必然是10000。
public int inc = 0; public synchronized void increase() { inc++; }
内存可见性
在Java中,咱们都知道关键字synchronized能够用于实现线程间的互斥,但咱们却经常忘记了它还有另一个做用,那就是确保变量在内存的可见性 - 即当读写两个线程同时访问同一个变量时,synchronized用于确保写线程更新变量后,读线程再访问该 变量时能够读取到该变量最新的值。
即当ThreadA释放锁M时,它所写过的变量(好比,x和y,存在它工做内存中的)都会同步到主存中,而当ThreadB在申请同一个锁M时,ThreadB的工做内存会被设置为无效,而后ThreadB会从新从主存中加载它要访问的变量到它的工做内存中(这时x=1,y=1,是ThreadA中修改过的最新的值)。经过这样的方式来实现ThreadA到ThreadB的线程间的通讯。
//线程A,B共同访问的代码 Object lock = new Object(); int a=0; int b=0; int c=0; //线程A,调用以下代码 synchronized(lock){ a=1; //1 b=2; //2 } //3 c=3; //4 //线程B,调用以下代码 synchronized(lock){ //5 System.out.println(a); //6 System.out.println(b); //7 System.out.println(c); //8 }
咱们假设线程A先运行,分别给a,b,c三个变量进行赋值(注:变量a,b的赋值是在同步语句块中进行的),而后线程B再运行,分别读取出这三个变量的值并打印出来。那么线程B打印出来的变量a,b,c的值分别是多少?
输出结果:
线程B里,打印的a,b确定是1和2. 可是,访问的到c变量有可能仍是0,而不是3.
(1)从而咱们能够看出volatile虽然具备可见性可是并不能保证原子性。
(2)性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些状况下性能要优于synchronized。
可是要注意volatile关键字是没法替代synchronized关键字的,由于volatile关键字没法保证操做的原子性。