上期回顧:
上次博客我們說了我們的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; } }
很簡單,也不是屬於我們多線程范疇該說的,這里就是帶着說了一下,就是當我們調用內部方法時,會主動觸發對象的創建,這樣就是餓漢模式。