單例bean與類載入過程

2023-05-31 18:03:27

構造單例bean的方式有很多種,我們來看一下其中一種,餓漢式

public class Singleton1 implements Serializable {
    //1、建構函式私有
    private Singleton1() {
        if (INSTANCE != null) {
            throw new RuntimeException("單例物件不能重複建立");
        }
        System.out.println("private Singleton1()");
    }
//2、建立靜態常數物件,Instance
    private static final Singleton1 INSTANCE = new Singleton1();
//3、使用getInstance()獲取物件
    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }

    public Object readResolve() {
        return INSTANCE;
    }
}

其保證了單例bean的特性如下:

1、建構函式私有

2、建立靜態常數物件,Instance

3、使用getInstance()獲取物件

並且對單例bean被破壞進行了防範:

  • 構造方法丟擲異常是防止反射破壞單例
  • readResolve() 是防止反序列化破壞單例

目前來看,都沒什麼問題,但是如果我想建立兩個靜態變數 a與b呢,並且在new的時候對a,b進行++,會發生什麼?

程式碼如下:

public class Singleton1 implements Serializable {
    //1、建構函式私有
    private Singleton1() {
        if (INSTANCE != null) {
            throw new RuntimeException("單例物件不能重複建立");
        }
        //呼叫建構函式時會對a與b進行++;
        a++;
        b++;
        System.out.println("private Singleton1()");
    }
    //2、建立靜態常數物件,Instance
    private static final Singleton1 INSTANCE = new Singleton1();
    public static int a;
    public static int b=0;
    //3、使用getInstance()獲取物件
    public static Singleton1 getInstance() {

        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }

    public Object readResolve() {
        return INSTANCE;
    }
}

這時如果我想對a與b進行輸出,最終a、b的值會是多少?

public class TestCase {

    public static void main(String[] args) {
        Singleton1 instance = Singleton1.getInstance();
        System.out.println("a="+instance.a+"  b="+instance.b);

    }
}

想必很多人會認為答案是a與b各自+1;輸出 a=1 b=1;

但是真相卻是:

private Singleton1()
a=1  b=0

這時為什麼呢?

這就與類載入機制有關了,首先我們得知道類載入過程,在類初始化之前,會有一個連結階段,其中有一個步驟叫做準備

而在準備階段將會:

  1. 為類變數(static變數)分配記憶體並且設定該類變數的預設初始值,即零值
  2. 這裡不包含用final修飾的static,因為final在編譯的時候就會分配好了預設值,準備階段會顯式初始化
  3. 注意:這裡不會為範例變數分配初始化,類變數會分配在方法區中,而範例變數是會隨著物件一起分配到Java堆中

所以a與b在準備階段被預設初始值為0;

接下來會進行類初始化,而類初始化的時機則有如下7種:

  1. 建立類的範例
  2. 存取某個類或介面的靜態變數,或者對該靜態變數賦值
  3. 呼叫類的靜態方法
  4. 反射(比如:Class.forName(「com.atguigu.Test」))
  5. 初始化一個類的子類
  6. Java虛擬機器器啟動時被標明為啟動類的類
  7. JDK7開始提供的動態語言支援:java.lang.invoke.MethodHandle範例的解析結果REF_getStatic、REF putStatic、REF_invokeStatic控制程式碼對應的類沒有初始化,則初始化

很顯然我們執行Singleton1 instance = Singleton1.getInstance();時,便對應上面第2點。所以會進行類初始化。

而在類初始化階段也就是clinit():

  1. 初始化階段就是執行類構造器方法<clinit>()的過程

  2. 此方法不需定義,是javac編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併而來。也就是說,當我們程式碼中包含static變數的時候,就會有clinit方法

  3. <clinit>()方法中的指令按語句在原始檔中出現的順序執行

  4. <clinit>()不同於類的構造器。(關聯:構造器是虛擬機器器視角下的<init>()

  5. 若該類具有父類別,JVM會保證子類的<clinit>()執行前,父類別的<clinit>()已經執行完畢

  6. 虛擬機器器必須保證一個類的<clinit>()方法在多執行緒下被同步加鎖

可以看到第3點,會跟據原始檔種的出現順序執行:

我們再看看最開始的程式碼中靜態常數與變數的先後順序:

  private static final Singleton1 INSTANCE = new Singleton1();
    public static int a;
    public static int b=0;

相比大家已經恍然大悟,對於答案也呼之欲出了。

沒錯,當在準備階段,a,b將會被賦預設值為0,而當我們呼叫getInstance()時,就會觸發類載入的過程,按照原始碼的先後順序,先執行new Singleton1(),將會對a與b進行++,所以a與b分別為1。之後繼續順序執行,int a;不會改變a的值,而b=0,則重新將b從1覆蓋為0了。所以最終我們顯示a=1 b=0;

那麼如何解決呢?

沒錯!只要修改一下變數宣告的順序,將a與b宣告在INSTANCE之前,就不會出現a,b資料不一致的問題了!

謝謝大家閱讀,才疏學淺,望多多指教!