java架構之路(多執行緒)大廠方式手寫單例模式
上期回顧:
上次部落格我們說了我們的volatile關鍵字,我們知道volatile可以保證我們變數被修改馬上刷回主存,並且可以有效的防止指令重排序,思想就是加了我們的記憶體屏障,再後面的多執行緒部落格裡還有說到很多的屏障問題。
volatile雖然好用,但是別用的太多,咱們就這樣想啊,一個被volatile修飾的變數持續性的在修改,每次修改都要及時的刷回主記憶體,我們講JMM時,我們的CPU和主記憶體之間是通過匯流排來連線的,也就是說,每次我們的volatile變數改變了以後都需要經過匯流排,“道路就那麼寬,持續性的通車”,一定會造成堵車的,也就是我們的說的匯流排風暴。所以使用volatile還是需要注意的。
單例模式:
屬於建立型別的一種常用的軟體設計模式。通過單例模式的方法建立的類在當前程序中只有一個例項(根據需要,也有可能一個執行緒中屬於單例,如:僅執行緒上下文內使用同一個例項),就是說每次我們建立的物件成功以後,在一個執行緒中有且僅有一個物件在正常使用。可以分為懶漢式和餓漢式。
懶漢式就是什麼意思呢,建立時並沒有例項化物件,而是呼叫時才會被例項化。我們來看一下簡單的程式碼。
public class LasySingletonMode { public static void main(String[] args) { LasySingleton instnace = LasySingleton.getInstnace(); } } class LasySingleton { /** * 私有化構造方法,禁止外部直接new物件 */ private LasySingleton() { } /** * 給予一個物件作為返回值使用 */ private static LasySingleton instnace; /** * 給予一個獲取物件的入口 * * @return LasySingleton物件 */ public static LasySingleton getInstnace() { if (null == instnace) { instnace = new LasySingleton(); } return instnace; } }
看起來很簡單的樣子,私有化構造方法,給予入口,返回物件,差不多就這樣就可以了,但是有一個問題,如果是多執行緒呢?
public static LasySingleton getInstnace() { if (null == instnace) { instnace = new LasySingleton(); } return instnace; }
我們假想兩個執行緒,要一起執行這段程式碼,執行緒A進來了,看到instnace是null的,ε=(´ο`*)))唉,執行緒B進來看見instnace也是null的(因為執行緒A還沒有執行到instnace = new LasySingleton()這個程式碼),這時就會造成執行緒A,B建立了兩個物件出來,也就不符合我們的單例模式了,我們來改一下程式碼。
public static LasySingleton getInstnace() { if (null == instnace) { synchronized (LasySingleton.class){ instnace = new LasySingleton(); } } return instnace; }
這樣貌似就可以了,就算是兩個執行緒進來,也只有一個物件可以拿到synchronized鎖,就不會產生new 兩個物件的行為了,其實不然啊,我們還是兩個執行緒來訪問我們的這段程式碼,執行緒A和執行緒B,兩個執行緒來了一看,物件是null的,需要建立啊,於是執行緒A拿到鎖,開始建立,執行緒B繼續等待,執行緒A建立完成,返回物件,將鎖釋放,這時執行緒B可以獲取到鎖(因為null == instnace判斷已經通過了,在if裡面進行的執行緒等待),這時執行緒B還是會建立一個物件的,這顯然還是不符合我們的單例模式啊,我們來繼續改造。
public static LasySingleton getInstnace() { if (null == instnace) { synchronized (LasySingleton.class){ if (null == instnace) { instnace = new LasySingleton(); } } } return instnace; }
這次基本就可以了吧,回想一下我們上次的volatile有序性,難道真的這樣就可以了嗎?instnace = new LasySingleton()是一個原子操作嗎?有時候你面試小廠,這樣真的就可以了,我們來繼續深挖一下程式碼。看一下程式的彙編指令碼,首先找我們的class檔案。執行javap -c ****.class。
E:\IdeaProjects\tuling-mvc-3\target\classes\com\tuling\control>javap -c LasySingleton.class Compiled from "LasySingletonMode.java" class com.tuling.control.LasySingleton { public static com.tuling.control.LasySingleton getInstnace(); Code: 0: aconst_null 1: getstatic #2 // Field instnace:Lcom/tuling/control/LasySingleton; 4: if_acmpne 17 7: new #3 // class com/tuling/control/LasySingleton 10: dup 11: invokespecial #4 // Method "<init>":()V 14: putstatic #2 // Field instnace:Lcom/tuling/control/LasySingleton; 17: getstatic #2 // Field instnace:Lcom/tuling/control/LasySingleton; 20: areturn }
不是很好理解啊,我們只想看instnace = new LasySingleton()是不是一個原子操作,我們可以這樣來做,建立一個最簡單的類。
public class Demo { public static void main(String[] args) { Demo demo = new Demo(); } }
然後我們執行javap -c -v ***.class
E:\IdeaProjects\tuling-mvc-3\target\classes>javap -c -v Demo.class Classfile /E:/IdeaProjects/tuling-mvc-3/target/classes/Demo.class Last modified 2020-1-13; size 389 bytes MD5 checksum f8b222a4559c4bf7ea05ef086bd3198c Compiled from "Demo.java" public class Demo minor version: 0 major version: 49 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#19 // java/lang/Object."<init>":()V #2 = Class #20 // Demo #3 = Methodref #2.#19 // Demo."<init>":()V #4 = Class #21 // java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 LocalVariableTable #10 = Utf8 this #11 = Utf8 LDemo; #12 = Utf8 main #13 = Utf8 ([Ljava/lang/String;)V #14 = Utf8 args #15 = Utf8 [Ljava/lang/String; #16 = Utf8 demo #17 = Utf8 SourceFile #18 = Utf8 Demo.java #19 = NameAndType #5:#6 // "<init>":()V #20 = Utf8 Demo #21 = Utf8 java/lang/Object { public Demo(); 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 1: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LDemo; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #2 // class Demo 3: dup 4: invokespecial #3 // Method "<init>":()V 7: astore_1 8: return LineNumberTable: line 3: 0 line 4: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; 8 1 1 demo LDemo; } SourceFile: "Demo.java" E:\IdeaProjects\tuling-mvc-3\target\classes>
結果是這樣的,我們來分析一下程式碼,先看這個
0: new #2 // class Demo
就是什麼意思呢?我們要給予Demo物件在對空間上開闢一個空間,並且返回記憶體地址,指向我們的運算元棧的Demo物件
3: dup
是一個物件複製的過程。
4: invokespecial #3 // Method "<init>":()V
見名知意,init是一個初始化過程,我們會把我們的剛才開闢的棧空間進行一個初始化,
7: astore_1
這個就是一個賦值的過程,剛才我們有個複製的操作對吧,這時會把我們複製的一個物件賦值給我們的棧空間上的Demo,是不是有點蒙圈了,別急,後面的簡單。
這是一個物件的初始化過程,在我的JVM系列部落格簡單的說過一點,後面我會詳細的去說這個,總結起來就是三個過程。
1.開闢空間 2.初始化空間 3.給引用賦值
這個程式碼一般情況下,會按照123的順序去執行的,但是超高併發的場景下,可能會變為132,考慮一下是不是,我們的as-if-serial,132的執行順序在單執行緒的場景下也是合理的,如果真的出現了132的情況,會造成什麼後果呢?回到我們的單例模式,所以說我們上面單例模式程式碼還需要改。
public class LasySingletonMode { public static void main(String[] args) { LasySingleton instnace = LasySingleton.getInstnace(); } } class LasySingleton { /** * 私有化構造方法,禁止外部直接new物件 */ private LasySingleton() { } /** * 給予一個物件作為返回值使用 */ private static volatile LasySingleton instnace; /** * 給予一個獲取物件的入口 * * @return LasySingleton物件 */ public static LasySingleton getInstnace() { if (null == instnace) { synchronized (LasySingleton.class) { if (null == instnace) { instnace = new LasySingleton(); } } } return instnace; } }
這樣來寫,就是一個滿分的單例模式了,無論出於什麼樣的考慮,都是滿足條件的。也說明你真的理解了我們的volatile關鍵字。
餓漢式相當於懶漢式就簡單很多了,不需要考慮那麼多了。
package com.tuling.control; public class HungrySingletonMode { public static void main(String[] args) { String name = HungrySingleton.name; System.out.println(name); } } class HungrySingleton { /** * 私有化構造方法,禁止外部直接new物件 */ private HungrySingleton() { } private static HungrySingleton instnace = new HungrySingleton(); public static String name = "XXX"; static{ System.out.println("我被建立了"); } public static HungrySingleton getInstance(){ return instnace; } }
很簡單,也不是屬於我們多執行緒範疇該說的,這裡就是帶著說了一下,就是當我們呼叫內部方法時,會主動觸發物件的建立,這樣就是餓漢模式。