關於Volatile關鍵字的研究

問題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秒。起十個線程,定時的操做。
java

有經驗的同窗一看就知道,這麼操做以爲線程不安全。讓咱們看看執行的結果。安全

0 9 [pool-1-thread-9]多線程

0 8 [pool-1-thread-7]併發

0 7 [pool-1-thread-5]ide

0 6 [pool-1-thread-3]性能

0 5 [pool-1-thread-1]測試

0 4 [pool-1-thread-2]this

0 3 [pool-1-thread-4]spa

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. 參數的讀取,併發性和實時性很是好。

相關文章
相關標籤/搜索