1. 程式人生 > >java架構之路(多執行緒)大廠方式手寫單例模式

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;
    }
}

  很簡單,也不是屬於我們多執行緒範疇該說的,這裡就是帶著說了一下,就是當我們呼叫內部方法時,會主動觸發物件的建立,這樣就是餓漢模式。