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

  很簡單,也不是屬於我們多線程範疇該說的,這裏就是帶着說了一下,就是當我們調用內部方法時,會主動觸發對象的創建,這樣就是餓漢模式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章