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的修改都能迅速的讓其餘線程感知到。也就是線程間的可見性。
但幾回併發之後,它就忍不住線程不安全了。可見並無保證線程的安全。
在解答這個問題以前,先說一下JAVA的內存模型,先看一張圖
JAVA中的內存主要分主內存和線程工做內存。
主內存就是平時談論最多的JVM的內存。
線程工做內存就是咱們平時所說的線程獨享內存。你們都知道每一個線程有本身一塊單獨的內存。
每一次任務的執行都要執行以上幾個操做(Read,load,use,asign,store,write)。
如圖所示,其中load,use,asign,store動做都是在線程獨享內存中發生的,並不會同步到主內存中。最後write時纔會寫會到主內存。
因此,在load,use,asign,store中變量的修改都是隻發生在當前內存的,並不會被其餘線程所看到,由於是線程獨享的。
那麼Volatile關鍵字的做用就是在load,use,asign,store動做的時候當即會將值同步到主內存,讓其餘線程當即能夠看到。這也就是上面所說的可見性。
雖然保證了可見性,但並無作互斥的保證,這也就是爲何多線程併發的時候,並不能保證線程的原子性。
使用Volatile有兩個條件:
該變量的寫操做不依賴當前的值
該變量沒有包含在其餘變量的不變式中
第一個比較好理解,例如++操做,就不符合第一個要求。由於++會先讀取再寫入。顯然依賴了當前的值。
因此最開始咱們的例子當中,對於volatile修飾的變量作了++和--的操做顯然是不合適的。
第二個舉個例子
private volatile int volatileCounter = 1; private final int total = 100 + volatileCounter;
假設咱們有一個變量叫total,是100+volatileCounter的值。這樣作也是不合適的。由於違反了第二條約定。
結合上面提到的兩個使用條件,使用volatile做爲標誌位是很是合適的,並且會比使用synchronized修飾會容易和效率的多。
volatile boolean shutdownRequested = false; public void shutdown(){ shutdownRequested = true; } public void doWork(){ while(shutdownRequested){ //do shutdown } }
在多線程環境下,爲了不多個線程同時去作關閉動做。能夠用一個volatile修飾的shutdownRequested標誌。這種作法要比使用synchronized容易和高效得多。
最經典的例子就是單例模式。若是要保證併發狀況下單例,能夠用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; }
獨立觀察有點像溫度觀測站,一邊負責收集溫度,一邊負責按期的彙報當前溫度
private volatile String temperature; //彙報當前的溫度 public String getReport(){ return "當前溫度是"+temperature+"度"; } //收集當前溫度,能夠多個站點併發的收集 private void doCollect(){ while(true){ String currentTemperature = getTemp(); temperature = currentTemperature; } }
既然一個參數能夠是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; } }
當讀的調用量遠遠超過寫的時候,咱們能夠考慮使用內部鎖和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++; }
上面五個場景可能會有人說都比較相似或者接近。若是仔細觀察能夠發現,都有幾個共同的特色:
或者對於參數的讀取,並不存在依賴性(指依賴上一次的結果)
對於寫入的方法仍是須要併發的控制,若是要作依賴的操做,如++,單例。若是是獨立的操做,不依賴以前的結果,能夠不用作併發控制。
參數的讀取,併發性和實時性很是好。