編程系列2】volatile)
主要講解volatile的相關(guān)知識(shí)以及容易遇到的坑。volatile變量的特性保證可見性不保證原子性當(dāng)寫一個(gè)volatile變量時(shí)JMM會(huì)把該線程本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去這個(gè)寫操作會(huì)導(dǎo)致其他線程中的volatile變量緩存無效。禁止指令重排我們回顧一下重排序需要遵守一定規(guī)則重排序操作不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。比如a1;ba; 這個(gè)指令序列由于第二個(gè)操作依賴于第一個(gè)操作所以在編譯時(shí)和處理器運(yùn)行時(shí)這兩個(gè)操作不會(huì)被重排序。重排序是為了優(yōu)化性能但是不管怎么重排序單線程下程序的執(zhí)行結(jié)果不能被改變。比如a1;b2;cab這三個(gè)操作第一步a1)和第二步(b2)由于不存在數(shù)據(jù)依賴關(guān)系 所以可能會(huì)發(fā)生重排序但是cab這個(gè)操作是不會(huì)被重排序的因?yàn)樾枰WC最終的結(jié)果一定是cab3。volatile禁止指令重排規(guī)則使用volatile關(guān)鍵字修飾共享變量便可以禁止這種重排序。若用volatile修飾共享變量在編譯時(shí)會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序volatile禁止指令重排序也有一些規(guī)則當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí)在其前面的操作的更改肯定全部已經(jīng)進(jìn)行且結(jié)果已經(jīng)對(duì)后面的操作可見在其后面的操作肯定還沒有進(jìn)行在進(jìn)行指令優(yōu)化時(shí)不能將對(duì)volatile變量訪問的語句放在其后面執(zhí)行也不能把volatile變量后面的語句放到其前面執(zhí)行。即執(zhí)行到volatile變量時(shí)其前面的所有語句都執(zhí)行完后面所有語句都未執(zhí)行。且前面語句的結(jié)果對(duì)volatile變量及其后面語句可見。volatile禁止指令重排分析該部分相關(guān)內(nèi)容我直接copy上一篇文章不是為了湊篇幅因?yàn)橛型瑢W(xué)沒有看上一篇文章直接看這篇為了能讓每一篇文章能獨(dú)立成章可能會(huì)引用之前文章中的內(nèi)容。先看下面未使用volatile的代碼class ReorderExample { int a 0; boolean flag false; public void writer() { a 1; //1 flag true; //2 } Public void reader() { if (flag) { //3 int i a * a; //4 System.out.println(i); } } }因?yàn)橹嘏判蛴绊懰宰罱K的輸出可能是0具體分析請(qǐng)參考我的上一篇文章《Java并發(fā)編程系列1-基礎(chǔ)知識(shí)》如果引入volatile我們?cè)倏匆幌麓aclass ReorderExample { int a 0; boolean volatile flag false; public void writer() { a 1; //1 flag true; //2 } Public void reader() { if (flag) { //3 int i a * a; //4 System.out.println(i); } } }這個(gè)時(shí)候volatile禁止指令重排序也有一些規(guī)則這個(gè)過程建立的happens before關(guān)系可以分為兩類根據(jù)程序次序規(guī)則1 happens before 2; 3 happens before 4。根據(jù)volatile規(guī)則2 happens before 3。根據(jù)happens before 的傳遞性規(guī)則1 happens before 4。上述happens before關(guān)系的圖形化表現(xiàn)形式如下在上圖中每一個(gè)箭頭鏈接的兩個(gè)節(jié)點(diǎn)代表了一個(gè)happens before 關(guān)系。黑色箭頭表示程序順序規(guī)則橙色箭頭表示volatile規(guī)則藍(lán)色箭頭表示組合這些規(guī)則后提供的happens before保證。這里A線程寫一個(gè)volatile變量后B線程讀同一個(gè)volatile變量。A線程在寫volatile變量之前所有可見的共享變量在B線程讀同一個(gè)volatile變量后將立即變得對(duì)B線程可見。volatile不適用場(chǎng)景volatile不適合復(fù)合操作下面是變量自加的示例public class volatileTest { public volatile int inc 0; public void increase() { inc; } public static void main(String[] args) { final volatileTest test new volatileTest(); for(int i0;i10;i){ new Thread(){ public void run() { for(int j0;j1000;j) test.increase(); }; }.start(); } while(Thread.activeCount()1) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(inc output: test.inc); } }測(cè)試輸出inc output:8182因?yàn)閕nc不是一個(gè)原子性操作可以由讀取、加、賦值3步組成所以結(jié)果并不能達(dá)到10000。解決方法采用synchronizedpublic class volatileTest1 { public int inc 0; public synchronized void increase() { inc; } public static void main(String[] args) { final volatileTest1 test new volatileTest1(); for(int i0;i10;i){ new Thread(){ public void run() { for(int j0;j1000;j) test.increase(); }; }.start(); } while(Thread.activeCount()1) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(add synchronized, inc output: test.inc); } }采用Lockpublic class volatileTest2 { public int inc 0; Lock lock new ReentrantLock(); public void increase() { lock.lock(); inc; lock.unlock(); } public static void main(String[] args) { final volatileTest2 test new volatileTest2(); for(int i0;i10;i){ new Thread(){ public void run() { for(int j0;j1000;j) test.increase(); }; }.start(); } while(Thread.activeCount()1) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(add lock, inc output: test.inc); } }采用AtomicIntegerpublic class volatileTest3 { public AtomicInteger inc new AtomicInteger(); public void increase() { inc.getAndIncrement(); } public static void main(String[] args) { final volatileTest3 test new volatileTest3(); for(int i0;i10;i){ new Thread(){ public void run() { for(int j0;j100;j) test.increase(); }; }.start(); } while(Thread.activeCount()1) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(add AtomicInteger, inc output: test.inc); } }三者輸出都是1000如下add synchronized, inc output:1000 add lock, inc output:1000 add AtomicInteger, inc output:1000單例模式的雙重鎖為什么要加volatile先看一下單例代碼public class penguin { private static volatile penguin m_penguin null; // 避免通過new初始化對(duì)象 private void penguin() {} public void beating() { System.out.println(打豆豆); }; public static penguin getInstance() { //1 if (null m_penguin) { //2 synchronized(penguin.class) { //3 if (null m_penguin) { //4 m_penguin new penguin(); //5 } } } return m_penguin; //6 } }在并發(fā)情況下如果沒有volatile關(guān)鍵字在第5行會(huì)出現(xiàn)問題。instance new TestInstance();可以分解為3行偽代碼a. memory allocate() //分配內(nèi)存 b. ctorInstanc(memory) //初始化對(duì)象 c. instance memory //設(shè)置instance指向剛分配的地址上面的代碼在編譯運(yùn)行時(shí)可能會(huì)出現(xiàn)重排序從a-b-c排序?yàn)閍-c-b。在多線程的情況下會(huì)出現(xiàn)以下問題。當(dāng)線程A在執(zhí)行第5行代碼時(shí)B線程進(jìn)來執(zhí)行到第2行代碼。假設(shè)此時(shí)A執(zhí)行的過程中發(fā)生了指令重排序即先執(zhí)行了a和c沒有執(zhí)行b。那么由于A線程執(zhí)行了c導(dǎo)致instance指向了一段地址所以B線程判斷instance不為null會(huì)直接跳到第6行并返回一個(gè)未初始化的對(duì)象。總結(jié)volatile可以保證線程可見性且提供了一定的有序性但是無法保證原子性。在JVM底層volatile是采用“內(nèi)存屏障”來實(shí)現(xiàn)的。觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn)加入volatile關(guān)鍵字時(shí)會(huì)多出一個(gè)lock前綴指令lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障也稱內(nèi)存柵欄內(nèi)存屏障會(huì)提供3個(gè)功能它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置也不會(huì)把前面的指令排到內(nèi)存屏障的后面即在執(zhí)行到內(nèi)存屏障這句指令時(shí)在它前面的操作已經(jīng)全部完成它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫入主存如果是寫操作它會(huì)導(dǎo)致其他CPU中對(duì)應(yīng)的緩存行無效。最后也講解了volatile不適用的場(chǎng)景以及解決的方法并解釋了單例模式為何需要使用volatile。