這篇筆記是我《沒內鬼》系列第二篇,其實我計劃是把設計模式和多線程併發分爲兩個系列,統一叫《一塊兒學系列》來系統的介紹java
相關的知識,可是想到這篇筆記去年就寫成了,一直不發心也癢癢,因此整理一番就發出來,但願你們指正~ 面試
另外推薦我上一篇爆文
:沒內鬼,來點乾貨!SQL優化和診斷設計模式
一塊兒學習,一塊兒進步!緩存
volatile關鍵字是在通常面試中常常問到的一個點,你們對它的回答莫過於兩點:安全
那爲了更有底氣,那我們就來深刻看看吧微信
我們在聊volatile關鍵字的時候,首先須要瞭解JMM內存模型,它自己是一種抽象的概念並不真實存在,草圖以下:多線程
JMM內存模型規定了線程的工做機理:即全部的共享變量都存儲在主內存,若是線程須要使用,則拿到主內存的副本,而後操做一番,再放到主內存裏面去
併發
這個能夠引起一個思考,這是否是就是多線程併發狀況下線程不安全的根源?假如全部線程都操做主內存的數據,是否是就不會有線程不安全的問題,隨即引起下面的問題app
關於這個問題,我感受過於硬核,我只能簡單的想象假如沒有JMM,全部線程能夠直接操做主內存的數據會怎麼樣
ide
因此我想面對這樣的場景,前輩們才模仿CPU解決緩存一致性的思路肯定了JMM模型(能力不足,純屬猜想)
在多處理器系統中,每一個處理器都有本身的高速緩存,而他們又共享同一主存
咱們來看一段代碼:
public class VolatileTest { static volatile String key; public static void main(String[] args){ key = "Happy Birthday To Me!"; } }
經過對代碼進行javap命令,獲取其字節碼,內容以下(能夠忽略啦):
public class com.mine.juc.lock.VolatileTest minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#21 // java/lang/Object."<init>":()V #2 = String #22 // Happy Birthday To Me! #3 = Fieldref #4.#23 // com/mine/juc/lock/VolatileTest.key:Ljava/lang/String; #4 = Class #24 // com/mine/juc/lock/VolatileTest #5 = Class #25 // java/lang/Object #6 = Utf8 key #7 = Utf8 Ljava/lang/String; #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 LocalVariableTable #13 = Utf8 this #14 = Utf8 Lcom/mine/juc/lock/VolatileTest; #15 = Utf8 main #16 = Utf8 ([Ljava/lang/String;)V #17 = Utf8 args #18 = Utf8 [Ljava/lang/String; #19 = Utf8 SourceFile #20 = Utf8 VolatileTest.java #21 = NameAndType #8:#9 // "<init>":()V #22 = Utf8 Happy Birthday To Me! #23 = NameAndType #6:#7 // key:Ljava/lang/String; #24 = Utf8 com/mine/juc/lock/VolatileTest #25 = Utf8 java/lang/Object { static volatile java.lang.String key; descriptor: Ljava/lang/String; flags: ACC_STATIC, ACC_VOLATILE public com.mine.juc.lock.VolatileTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 11: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/mine/juc/lock/VolatileTest; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: ldc #2 // String Happy Birthday To Me! 2: putstatic #3 // Field key:Ljava/lang/String; 5: return LineNumberTable: line 16: 0 line 17: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 args [Ljava/lang/String; } SourceFile: "VolatileTest.java"
請你們注意這一段代碼:
static volatile java.lang.String key; descriptor: Ljava/lang/String; flags: ACC_STATIC, ACC_VOLATILE
能夠看到,volatile關鍵字在編譯的時候會主動爲變量增長標識:ACC_VOLATILE
,再研究下去就過於硬核了(彙編指令),我可能硬不起來(手動狗頭),之後我會再對它進行深刻的研究,咱們只用瞭解到,Java關鍵字volatile,是在編譯階段主動爲變量增長了ACC_VOLATILE標識,以此保證了它的內存可見性
即然volatile能夠保證內存可見性,那至少有一個場景咱們是能夠放心使用的,即:一寫多讀場景
另外,你們在驗證volatile內存可見性的時候,不要使用 System.out.println() ,緣由以下:
public void println() { newLine(); } /** * 是否是赫然看到一個synchronized,具體緣由見下文 */ private void newLine() { try { synchronized (this) { ensureOpen(); textOut.newLine(); textOut.flushBuffer(); charOut.flushBuffer(); if (autoFlush) out.flush(); } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } }
爲了優化程序性能,編譯器和處理器會對Java編譯後的字節碼和機器指令進行重排序,在單線程狀況下不會影響結果,然而在多線程狀況下,可能會出現莫名其妙的問題,案例見下文
運行這段代碼咱們可能會獲得一個匪夷所思的結果:咱們得到的單例對象是未初始化的。爲何會出現這種狀況?由於指令重排
首先要明確一點,同步代碼塊中的代碼也是可以被指令重排的。而後來看問題的關鍵
INSTANCE = new Singleton();
雖然在代碼中只有一行,編譯出的字節碼指令能夠用以下三行表示
因爲步驟2,3交換不會改變單線程環境下的執行結果,故而這種重排序是被容許的。也就是咱們在初始化對象以前就把INSTANCE變量指向了該對象。而若是這時另外一個線程恰好執行到代碼所示的2處
if (INSTANCE == null)
那麼這時候有意思的事情就發生了:雖然INSTANCE指向了一個未被初始化的對象,可是它確實不爲null了,因此這個判斷會返回false,以後它將return一個未被初始化的單例對象!
以下:
因爲重排序是編譯器和CPU自動進行的,如何禁止指令重排?
INSTANCE變量加個volatile關鍵字就行,這樣編譯器就會根據必定的規則禁止對volatile變量的讀寫操做重排序了。而編譯出的字節碼,也會在合適的地方插入內存屏障,好比volatile寫操做以前和以後會分別插入一個StoreStore屏障和StoreLoad屏障,禁止CPU對指令的重排序越過這些屏障
volatile 關鍵字雖然保證了內存可見,可是問題來了,見代碼:
index += 1;
這短短一行代碼在字節碼級別其實分爲了多個步驟進行,如獲取變量,賦值,計算等等,如CPU基本執行原理通常,真正執行的是一個個命令,分爲不少步驟
volatile 關鍵字能夠保證的是單個讀取操做是具備原子性的(每次讀取都是從主內存獲取最新的值)
可是如 index += 1; 實質是三個步驟,三次行爲,所以它沒法保證整塊代碼的原子性
首先駁斥一個關於類鎖的概念,synchronize就是對象鎖,在普通方法,靜態方法,同步塊時鎖的對象分別是:
類型 | 代碼示例 | 鎖住的對象 |
---|---|---|
普通方法 | synchronized void test() { } | 當前對象 |
靜態方法 | synchronized static void test() { } | 鎖的是當前類的Class 對象 |
同步塊 | void fun () { synchronized (this) {} } | 鎖的是()中的對象 |
你們都贊成在同步代碼塊中,鎖住的是括號裏的對象,那麼見如下代碼:
public class SynDemo { public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { synchronized (SynDemo.class) { System.out.println("真的有所謂的類鎖?"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); Thread.sleep(500); answer(); } synchronized static void answer () { System.out.println("答案清楚了嗎"); } } // 輸出結果 // 真的有所謂的類鎖? // 間隔2秒多左右 // 答案清楚了嗎
因此實際上所謂的類鎖,徹底就是當前類的Class對象,因此不要被誤導,synchronize就是對象鎖
JVM
是經過進入、退出對象監視器(Monitor
來實現對方法、同步塊的同步的
具體實現是在編譯以後在同步方法調用前加入一個 monitor.enter
指令,在退出方法和異常處插入 monitor.exit
的指令。
其本質就是對一個對象監視器 Monitor
進行獲取,而這個獲取過程具備排他性從而達到了同一時刻只能一個線程訪問的目的
而對於沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程 monitor.exit
以後才能嘗試繼續獲取鎖。
流程圖以下:
代碼例子:
public static void main(String[] args) { synchronized (Synchronize.class){ System.out.println("Synchronize"); } }
字節碼:
public class com.crossoverjie.synchronize.Synchronize { public com.crossoverjie.synchronize.Synchronize(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // class com/crossoverjie/synchronize/Synchronize 2: dup 3: astore_1 **4: monitorenter** 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #4 // String Synchronize 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: aload_1 **14: monitorexit** 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return Exception table: from to target type 5 15 18 any 18 21 18 any }
monitorexit
同步代碼塊添加了一個隱式的try-finally,在finally中會調用monitorexit
命令釋放鎖,目的是爲了不異常狀況就沒法釋放鎖
以前你們都說千萬不要用synchronized,效率太差啦,可是Hotspot團隊對synchronized進行許多優化,提供了三種狀態的鎖:偏向鎖、輕量級鎖、重量級鎖,這樣一來synchronized性能就有了極大的提升
偏向鎖:就是鎖偏向某一個線程。主要是爲了處理同一個線程屢次獲取同一個鎖的狀況,好比鎖重入或者一個線程頻繁操做同一個線程安全的容器,可是一旦出現線程之間競爭同一個鎖,偏向鎖就會撤銷,升級爲輕量級鎖
輕量級鎖:是基於CAS操做實現的。線程使用CAS嘗試獲取鎖失敗後,進行一段時間的忙等,也就是所謂的自旋操做。嘗試一段時間仍沒法獲取鎖纔會升級爲重量級鎖
重量級鎖:是基於底層操做系統實現的,每次獲取鎖失敗都會直接讓線程掛起,這會帶來用戶態
和內核態
的切換,性能開銷比較大
打一個比方:你們在排隊打飯,你有一個專屬通道,叫作帥哥美女專屬通道,只有你一我的能夠自由的同行,這就叫偏向鎖
忽然有一天,我來了,我也自誇帥哥,因此我盯上了你的通道,可是你還在打飯,而後我就搶過去和你一塊兒打飯,可是這樣效率比較低,因此阿姨沒問個人時候,我就玩會手機等你,這就叫輕量級鎖
忽然還有一天,我餓到不行,什麼帥哥美女通通滾蛋,就我一我的先打飯,全部阿姨爲我服務,給我服務完了再輪到大家,這就叫重量級鎖
這也就是上文提到的System.out.println()爲什麼會影響內存可見性的緣由了
字節碼獲取方法:
用法: javap <options> <classes> 其中, 可能的選項包括: -help --help -? 輸出此用法消息 -version 版本信息 -v -verbose 輸出附加信息 -l 輸出行號和本地變量表 -public 僅顯示公共類和成員 -protected 顯示受保護的/公共類和成員 -package 顯示程序包/受保護的/公共類 和成員 (默認) -p -private 顯示全部類和成員 -c 對代碼進行反彙編 -s 輸出內部類型簽名 -sysinfo 顯示正在處理的類的 系統信息 (路徑, 大小, 日期, MD5 散列) -constants 顯示最終常量 -classpath <path> 指定查找用戶類文件的位置 -cp <path> 指定查找用戶類文件的位置 -bootclasspath <path> 覆蓋引導類文件的位置
感謝如下博文及其做者:
文章中我留了一個小小的彩蛋,若是你能發現也證實你看的很是仔細啦
夏天到啦,加我微信,我來請你吃一根雪糕~ 僅限5.13日一天哦