关于Volatile关键字的研究

原创
2014/08/20 17:45
阅读数 841

问题1:Volatile有什么作用?

package com.victor.hello;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class VolatileTest {
	private static volatile int volatileCounter = 0;
	private static int noneVolatileCounter = 0;
	
	public static void main(String[] args){
		final ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
		for(int i =0;i<10;i++){
			service.scheduleAtFixedRate(new Runnable(){
				
				@Override
				public void run() {
					String threadName = Thread.currentThread().getName();
					volatileCounter++;
					sleep();
					volatileCounter--;
					noneVolatileCounter++;
					sleep();
					noneVolatileCounter--;
					System.out.println(volatileCounter+"	"+noneVolatileCounter+"	["+threadName+"]");
				}
				
			}, 0, 3, TimeUnit.SECONDS);
		}
	}
	
	private static void sleep(){
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

为了体验Volatile这个关键字的作用,我写了一个测试方法。两个int类型的变量,分别用volatile和不用volatile修饰。先做一个++的操作,再做一个--的操作。之间休息0.1秒。起十个线程,定时的操作。

有经验的同学一看就知道,这么操作觉得线程不安全。让我们看看执行的结果。

0 9 [pool-1-thread-9]

0 8 [pool-1-thread-7]

0 7 [pool-1-thread-5]

0 6 [pool-1-thread-3]

0 5 [pool-1-thread-1]

0 4 [pool-1-thread-2]

0 3 [pool-1-thread-4]

0 2 [pool-1-thread-8]

0 1 [pool-1-thread-6]

0 0 [pool-1-thread-10]

1 9 [pool-1-thread-3]

1 8 [pool-1-thread-5]

1 7 [pool-1-thread-9]

1 6 [pool-1-thread-7]

1 5 [pool-1-thread-2]

1 4 [pool-1-thread-4]

1 3 [pool-1-thread-1]

1 2 [pool-1-thread-6]

1 1 [pool-1-thread-8]

1 0 [pool-1-thread-10]

1 9 [pool-1-thread-1]

1 8 [pool-1-thread-3]

1 7 [pool-1-thread-5]

1 5 [pool-1-thread-9]

1 5 [pool-1-thread-7]

1 4 [pool-1-thread-4]

1 3 [pool-1-thread-8]

1 2 [pool-1-thread-2]

1 1 [pool-1-thread-6]

1 0 [pool-1-thread-10]

1 9 [pool-1-thread-7]

1 8 [pool-1-thread-5]

1 7 [pool-1-thread-9]

1 6 [pool-1-thread-8]

1 5 [pool-1-thread-4]

1 4 [pool-1-thread-1]

1 3 [pool-1-thread-2]

1 2 [pool-1-thread-3]

1 1 [pool-1-thread-6]

1 0 [pool-1-thread-10]

1 9 [pool-1-thread-3]

1 8 [pool-1-thread-7]

1 7 [pool-1-thread-5]

1 6 [pool-1-thread-4]

1 5 [pool-1-thread-8]

1 4 [pool-1-thread-1]

1 3 [pool-1-thread-9]

1 3 [pool-1-thread-2]

1 1 [pool-1-thread-6]

1 1 [pool-1-thread-10]

1 9 [pool-1-thread-7]

1 8 [pool-1-thread-3]

1 7 [pool-1-thread-9]

1 7 [pool-1-thread-5]

1 6 [pool-1-thread-1]

1 5 [pool-1-thread-4]

1 4 [pool-1-thread-8]

1 3 [pool-1-thread-2]

1 2 [pool-1-thread-6]

1 1 [pool-1-thread-10]

1 10 [pool-1-thread-4]

1 9 [pool-1-thread-7]

1 8 [pool-1-thread-1]

1 7 [pool-1-thread-3]

1 6 [pool-1-thread-8]

1 5 [pool-1-thread-2]

1 4 [pool-1-thread-9]

1 2 [pool-1-thread-5]

1 2 [pool-1-thread-10]

1 1 [pool-1-thread-6]

可见,每次并发的时候。Volatile的修改都能迅速的让其他线程感知到。也就是线程间的可见性。

但几次并发以后,它就忍不住线程不安全了。可见并没有保证线程的安全。

问题2:Volatile的原理?

在解答这个问题之前,先说一下JAVA的内存模型,先看一张图

JAVA中的内存主要分主内存和线程工作内存。

主内存就是平时谈论最多的JVM的内存。

线程工作内存就是我们平时所说的线程独享内存。大家都知道每个线程有自己一块单独的内存。

每一次任务的执行都要执行以上几个操作(Read,load,use,asign,store,write)。

如图所示,其中load,use,asign,store动作都是在线程独享内存中发生的,并不会同步到主内存中。最后write时才会写会到主内存。

所以,在load,use,asign,store中变量的修改都是只发生在当前内存的,并不会被其他线程所看到,因为是线程独享的。

那么Volatile关键字的作用就是在load,use,asign,store动作的时候立即会将值同步到主内存,让其他线程立即可以看到。这也就是上面所说的可见性。

虽然保证了可见性,但并没有做互斥的保证,这也就是为什么多线程并发的时候,并不能保证线程的原子性。

问题3:Volatile的使用场景?

使用Volatile有两个条件:

  1. 该变量的写操作不依赖当前的值

  2. 该变量没有包含在其他变量的不变式中

第一个比较好理解,例如++操作,就不符合第一个要求。因为++会先读取再写入。显然依赖了当前的值。

所以最开始我们的例子当中,对于volatile修饰的变量做了++和--的操作显然是不合适的。

第二个举个例子

private volatile int volatileCounter = 1;
private final int total = 100 + volatileCounter;

假设我们有一个变量叫total,是100+volatileCounter的值。这样做也是不合适的。因为违反了第二条约定。

场景1:状态标志

结合上面提到的两个使用条件,使用volatile作为标志位是非常合适的,而且会比使用synchronized修饰会容易和效率的多。

volatile boolean shutdownRequested = false;

public void shutdown(){
     shutdownRequested = true;    
}

public void doWork(){
    while(shutdownRequested){
        //do shutdown
    }
}

在多线程环境下,为了避免多个线程同时去做关闭动作。可以用一个volatile修饰的shutdownRequested标志。这种做法要比使用synchronized容易和高效得多。

场景2:一次性安全发布(one time safe publication)

最经典的例子就是单例模式。如果要保证并发情况下单例,可以用Volatile修饰。如下

//注意用volatile修饰
private volatile static Singleton singleton;

public static Singleton getInstance(){
    //第一次检查
    if(singleton == null){
        synchronized(Singleton.class){
            //第二次检查
            if(singleton == null) {
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

场景3:独立观察(independent observation)

独立观察有点像温度观测站,一边负责收集温度,一边负责定期的汇报当前温度

private volatile String temperature;

//汇报当前的温度
public String getReport(){
    return "当前温度是"+temperature+"度";
}

//收集当前温度,可以多个站点并发的收集
private void doCollect(){
    while(true){
        String currentTemperature = getTemp();
        temperature = currentTemperature;
    }        
}

场景4:Volatile Bean

既然一个参数可以是Volatile类型的,那么我们也可以构造一个volatile类型的bean. 很好理解,不再解释了。

@ThreadSafe  
public class Person {  
    private volatile String firstName;  
    private volatile String lastName;  
    private volatile int age;  
  
    public String getFirstName() 
    { 
        return firstName; 
    }
      
    public String getLastName() 
    { 
        return lastName; 
    }
      
    public int getAge() { 
        return age; 
    }  
  
    public void setFirstName(String firstName) {   
        this.firstName = firstName;  
    }  
  
    public void setLastName(String lastName) {   
        this.lastName = lastName;  
    }  
  
    public void setAge(int age) {   
        this.age = age;  
    }  
}

场景5:开销较低的“读写锁”

当读的调用量远远超过写的时候,我们可以考虑使用内部锁和volatile的组合来减少锁竞争带来的额外开销。

使用synchronized来控制自增的并发。但是getValue的方法只用了volatile修饰的返回值。大大的增加了并发量。因为synchronized每次只能有一个线程能访问,但是volatile却可以同时被多个线程访问。

@ThreadSafe  
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++;  
    }

总结

上面五个场景可能会有人说都比较类似或者接近。如果仔细观察可以发现,都有几个共同的特点:

  1. 或者对于参数的读取,并不存在依赖性(指依赖上一次的结果)

  2. 对于写入的方法还是需要并发的控制,如果要做依赖的操作,如++,单例。如果是独立的操作,不依赖之前的结果,可以不用做并发控制。

  3. 参数的读取,并发性和实时性非常好。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
26 收藏
0
分享
返回顶部
顶部