一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1、保证了不同线程对这个变量进行 *** 作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2、禁止进行指令重排序。
先看一段代码,假如线程1先执行,线程2后执行:
// 线程1 boolean stop = false; while(!stop){ doSomething(); } // 线程2 stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程。
在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
第一,使用volatile关键字会强制将修改的值立即写入主存
第二,使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)
第三,由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
先看代码:
public class Test { // volatile修饰 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<500;j++) test.increase(); }; }.start(); } try { Thread.sleep(3000); } catch(Exception e) { } System.out.println(test.inc); } }
上述代码的执行结果是一个不确定的数,是一个小于等于5000的数。为什么?因为inc++是不保证原子性的 *** 作,++ *** 作它包括读取变量的原始值、进行加1 *** 作、写入工作内存三步流程。假设线程A读取了inc变量的原始值,虽然是volatile,那就从主内存里直接读咯,读取完成后,这个时候线程A时间片走完,别的线程这个时候把这个值+1了,再回到线程A的时候,再对旧值+1,所以重复了,因此会得到比实际值5000小的值。
这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的 *** 作的原子性。
不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面说的volatile变量规则,但是要注意,线程A对变量进行读取 *** 作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。
怎么改进呢?
第一种,synchronized
public class Test { // volatile修饰 public volatile int inc = 0; // 加上synchronized关键字 public synchronized 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<500;j++) test.increase(); }; }.start(); } try { Thread.sleep(3000); } catch(Exception e) { } System.out.println(test.inc); } }
第二种,采用锁lock
public class Test { // volatile修饰 public volatile int inc = 0; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); // 加锁 try { inc++; } finally{ lock.unlock(); } } 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<500;j++) test.increase(); }; }.start(); } try { Thread.sleep(3000); } catch(Exception e) { } System.out.println(test.inc); } }
第三种,使用原子 *** 作类Atomic
public class Test { // 原子 *** 作类,底层使用CAS来进行原子性 *** 作 public AtomicInteger inc = new AtomicInteger(); public void increase() { inc.getAndIncrement(); } 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<500;j++) test.increase(); }; }.start(); } try { Thread.sleep(3000); } catch(Exception e) { } System.out.println(test.inc); } }3、Volatile能保证有序性吗?
// x、y为非volatile变量 // flag为volatile变量 x = 2; //语句1 y = 0; //语句2 flag = true; //语句3 x = 4; //语句4 y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
4、Volatile应用// DCL单例模式 public class PersonLazy4 { // 加上volatile关键字,最值得推荐的方式 private static volatile PersonLazy4 mInstance = null; private PersonLazy4() {} public static PersonLazy4 getInstance() { if (mInstance == null) { synchronized (PersonLazy4.class) { if (mInstance == null) { mInstance = new PersonLazy4(); } } } return mInstance; } }
部分内容转载于:http://www.cnblogs.com/dolphin0520/p/3920373.html
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)