在學習ConcurrentHashMap源碼的過程當中,發現本身對併發編程簡直是一無所知,所以打算從最基礎的volatile開始學習.html
volatile雖然很基礎,可是對於毫無JMM基礎的我來講,也是十分晦澀,看了許多文章仍然不能很好的表述出來.java
後來發現一篇文章(參考連接第一篇),給了我一些啓示:用回答問題的方式來學習知識及寫博客,由於對我這種新手來講,回答別人的問題,總比本身"演講"要來的容易許多.編程
volatile只能夠用來修飾變量,不能夠修飾方法以及類緩存
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
複製代碼
這是很經典的雙重鎖校驗實現的單例模式,想必不少人都看到過,代碼中可能會被多個線程訪問的singleton
變量使用volatile修飾.安全
當一個變量被volatile修飾時,會擁有兩個特性:多線程
JMM操做變量的時候不是直接在主存進行操做的,而是每一個線程擁有本身的工做內存,在使用前,將該變量的值copy一份到本身的工做內存,讀取時直接讀取本身的工做內存中的值.寫入操做時,先將修改後的值寫入到本身的工做內存,再講工做內存中的值刷新回主存.併發
相似於下圖: ide
爲何這麼搞呢?固然是爲了提升效率,畢竟主存的讀寫相較於CPU中的指令執行都太慢了.post
這樣就會帶來一個問題.當執行性能
i = i + 1;(i初始化爲0)
語句時,單線程操做固然沒有問題,可是若是兩個線程操做呢?獲得的結果是2嗎?
不必定.
讓咱們詳細分解一下執行這句話的操做.
讀取內存中的i=0到工做內存(1)
->工做內存中的i=i+1=1(2)
- > 將工做內存中的i=1刷新回主存(3)
.
這是單線程操做的狀況,那麼假設當線程1執行到了(2)
的時候,線程2開始了,進行完了(1)步驟,那麼這時候的狀況是什麼呢?
線程1位於(2)
,線程2位於(1)
.
線程1的工做內存中i=1,線程2的工做內存中i=0,以後分別進行餘下的步驟,最後拿到的結果爲1
.
這是什麼緣由形成的呢?由於普通的變量沒有保證內存可見性.即:線程1已經修改了i的值,其餘的線程卻沒有獲得這個消息.
volatile保證了這一點,用volatile修飾的變量,讀取操做與普通變量相同.可是寫入操做發生後會當即將其刷新回主存,而且使其餘線程中對這一變量的緩存失效!
緩存失效了怎麼辦呢?去再次讀取主存唄,主存此時已經修改了(當即刷新了),則保證了內存可見性.
####小栗子:
public class VolatileTest {
private static Boolean stop = false;//(1)
private static volatile Boolean stop = false;//(2)
public static void main(String args[]) throws InterruptedException {
//新創建一個線程
Thread testThread = new Thread() {
@Override
public void run() {
System.out.println();
int i = 1;
//不斷的對i進行自增操做
while (!stop) {
i++;
}
System.out.println("Thread stop i=" + i);
}
};
//啓動該線程
testThread.start();
//休眠一秒
Thread.sleep(1000);
//主線程中將stop置爲true
stop = true;
System.out.println(Thread.currentThread() + "now, in main thread stop is: " + stop);
testThread.join();
}
}
複製代碼
這段代碼在主線程的第二行定義了一個布爾變量stop, 而後主線程啓動一個新線程,在線程裏不停得增長計數器i的值,直到主線程的布爾變量stop被主線程置爲true才結束循環。
主線程用Thread.sleep停頓1秒後將布爾值stop置爲true。
所以,咱們指望的結果是,上述Java代碼執行1秒鐘後中止,而且打印出1秒鐘內計數器i的實際值。
然而,執行這個Java應用後,你發現它進入了死循環,程序沒有中止.
將(1)
處的代碼改成(2)
處的,即對stop的變量添加volatile修飾,你會發現程序如咱們預期的那樣中止了.
JVM在不影響單線程執行結果的狀況下回對指令進行重排序,好比:
int i = 1;//(1)
int j = 2;//(2)
int h = i * j;//(3)
複製代碼
上述代碼中,(3)執行依賴於(1)(2)的執行,可是(1)(2)的執行順序並不影響結果,也就是說當咱們進行了上述的編碼,JVM真正執行的多是(1)(2)(3),也多是(2)(1)(3).
這在單線程中是無所謂的,還會帶來性能的提高.
可是在多線程中就會出現問題,好比下面的代碼:
//線程1
context = loadContext();//(1)
inited = true;//(2)
//線程2
while(!inited ){ //根據線程A中對inited變量的修改決定是否使用context變量
sleep(100);
}
doSomethingwithconfig(context);
複製代碼
若是每一個線程中的指令都順序執行,則沒有問題,可是在線程1中,兩個語句並沒有依賴關係,所以可能會發生重排序,若是發生了重排序:
inited = true;//(2)
context = loadContext();//(1)
複製代碼
線程1重排序以後先執行了(2)語句,在線程2中,程序跳出了循環,執行doSomethingwithconfig
,由於他認爲context已經進行了初始化,而後並無,就會出現錯誤.
使用volatile關鍵字修飾inited
變量,JVM就會阻止對inited
相關的代碼進行重排序.這樣就可以按照既定的順序指執行.
volatile是輕量級同步機制,與synchronized相比,他的開銷更小一些,同時安全性也有所下降,在一些特定的場景下使用它能夠在完成併發目標的基礎上有一些性能上的優點.可是同時也會帶來一些安全上的問題,且比較難以排查,使用時須要謹慎.
使用volatile修飾的變量最好知足如下條件:
這裏舉幾個比較經典的場景:
volatile並不能保證操做的原子性,想要保證原子性請使用synchronized關鍵字加鎖.
www.techug.com/post/java-v… www.importnew.com/23535.html
完。
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客------>呼延十