volatile作用对象:Volatile只作用于共享变量。
共享变量:在多个线程之间能够被共享的变量被称为共享变量。共享变量包括所有的实例变量,静态变量和数组元素。他们都被存放在堆内存中,
volatile作用:
1、同步
同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。
如图,如果变量没有volatile关键字,那么A线程对该变量的改变存储在内存A,B变量不可知。
将一个共享变量声明为volatile后,会有以下效应:
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2.这个写会操作会导致其他线程中的缓存无效。
非原子操作出现的问题:
package test; import java.util.concurrent.CountDownLatch; /** * Created by chengxiao on 2017/3/18. */ public class Counter { public static volatile int num = 0; //使用CountDownLatch来等待计算线程执行完 static CountDownLatch countDownLatch = new CountDownLatch(30); public static void main(String []args) throws InterruptedException { //开启30个线程进行累加操作 for(int i=0;i<30;i++){ new Thread(){ public void run(){ for(int j=0;j<10000;j++){ num++;//自加操作 } countDownLatch.countDown(); } }.start(); } //等待计算线程执行完 countDownLatch.await(); System.out.println(num); } }
得到的结果并不是300000,而是224291,原因是num++不是个原子性的操作,而是个复合操作(读取、加1、赋值)。所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中。
解决方法:通过使用Automic原子操作类
/** * Created by chengxiao on 2017/3/18. */ public class Counter { //使用原子操作类 public static AtomicInteger num = new AtomicInteger(0); //使用CountDownLatch来等待计算线程执行完 static CountDownLatch countDownLatch = new CountDownLatch(30); public static void main(String []args) throws InterruptedException { //开启30个线程进行累加操作 for(int i=0;i<30;i++){ new Thread(){ public void run(){ for(int j=0;j<10000;j++){ num.incrementAndGet();//原子性的num++,通过循环CAS方式 } countDownLatch.countDown(); } }.start(); } //等待计算线程执行完 countDownLatch.await(); System.out.println(num); } }
2、禁止指令重排序优化
重排序在多线程中可能存在的问题:
public class TestVolatile { int a = 1; boolean status = false; /** * 状态切换为true */ public void changeStatus(){ a = 2;//1 status = true;//2 } /** * 若状态为true,则running。 */ public void run(){ if(status){//3 int b = a+1;//4 System.out.println(b); } } }
上述第1、2步由于不存在依赖关系,可能会被系统冲排序,从而不能保证第四步的b=3。
volatile使用场景:
必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
Eg1: 状态标志。使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如,shutdownRequested 标志从false 转换为true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期无需被察觉的情况下才能扩展。
Eg2: 一次性安全发布(one-time safe publication)。例如单例模式,这里volatile的作用仅仅是阻止指令重排序, 不涉及可见性问题, 可见性已经由synchronized来保证了。
* 初始化对象的指令可能会被重排成:
(1)分配内存空间。
(2)将内存空间的地址赋值给对应的引用。
(3)初始化对象
private volatile static Singleton instace; public static Singleton getInstance(){ //第一次null检查 if(instance == null){ synchronized(Singleton.class) { //1 //第二次null检查 if(instance == null){ //2 instance = new Singleton();//3 } } } return instance; }
Eg3: 独立观察(independent observation)。定期 “发布” 观察结果供程序内部其他线程读取这个变量。
Eg4: “volatile bean” 模式。在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的。
Eg5:开销较低的“读-写锁”策略。如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。开销较低的“读-写锁”策略
public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; //读操作,没有synchronized,提高性能 public int getValue() { return value; } //写操作,必须synchronized。因为x++不是原子操作 public synchronized int increment() { return value++; } }