單例模式的使用

jdk和Spring都有實現單例模式,這裡舉的例子是JDK中Runtime這個類

Runtime的使用

通過Runtime類可以獲取JVM堆記憶體的資訊,還可以呼叫它的方法進行GC。

public class Test {
public static void main(String[] args) throws Exception { Runtime runtime = Runtime.getRuntime();
runtime.gc();
//jvm的堆記憶體總量
System.out.println("堆記憶體總量" + runtime.totalMemory()/1024/1024 + "MB");
//jvm檢視使用的最大堆記憶體
System.out.println("最大堆記憶體" + runtime.maxMemory()/1024/1024 + "MB");
//jvm剩餘可用的記憶體
System.out.println("可用的記憶體" +runtime.freeMemory()/1024/1024 + "MB"); Runtime runtime1 = Runtime.getRuntime(); System.out.println(runtime == runtime1);
}
}

這裡建立了兩個物件,通過等於號判斷,兩個引用來自同一個物件,確實是單例模式

Runtime的定義

這個類是介紹是:每一個Java應用有一個Runtime的例項,可以獲取應用執行時的環境屬性,當前的例項通過

getRuntime方法獲取 。應用程式不能建立這個類的例項。

這差不多包含了單例類的定義,然後看一下這個類的內部實現

很明顯是一個標準的單例模式的(餓漢)實現,首先使用static修飾例項物件,所以類載入的時候就會建立例項,然後呼叫方法返回這個例項,使用private修飾建構函式。

反射破壞單例模式

Runtime類將建構函式私有化,就是不想讓人建立它的例項,但是我們卻可以使用反射來建立物件

public class Test {
public static void main(String[] args) throws Exception { Class<?> clazz = Runtime.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Object o1 = constructor.newInstance();
Object o2 = Runtime.getRuntime();
System.out.println(o1.getClass().getSimpleName());
System.out.println(o2.getClass().getSimpleName());
System.out.println(o1 == o2); }
}

通過執行結果可以看到,已經成功的建立了兩個Runtime物件

至於破壞Runtime類的單例有什麼壞處我也不知道,畢竟我是不會用反射去破壞它的,總之應該是有壞處的,下面看一下不能被反射破壞的單例模式實現

單例模式的實現

列舉類實現

使用列舉實現是因為JDK底層保護我們的列舉類不被反射,就解決了單例被反射破壞的問題

EnumSingleton.java

在列舉類中放了一個內部類(其實不放內部類也行)

public enum EnumSingleton {

    INSTANCE;
class MyRuntime{ public void hello(){
System.out.println("hello");
}
} private MyRuntime myRuntime; EnumSingleton(){
myRuntime = new MyRuntime();
} public MyRuntime getData(){
return myRuntime;
} public static EnumSingleton getInstance(){
return INSTANCE;
}
}

下面測試一下這個單例

public class Test {
public static void main(String[] args) throws Exception { EnumSingleton.MyRuntime myRuntime = EnumSingleton.INSTANCE.getData();
myRuntime.hello();
EnumSingleton.MyRuntime myRuntime1 = EnumSingleton.getInstance().getData();
System.out.println(myRuntime == myRuntime1);
}
}

結果顯而易見,單例模式已經成功實現

至於使用反射測試列舉類,可以直接看一下JDK對列舉類的一個保護

使用反射建立物件,即呼叫Construct類的newInstance方法,這個方法裡面已經定義了列舉物件不能被建立

使用列舉實現單例的壞處有

  • 因為很少使用列舉類,所以用列舉建立單例感覺挺奇怪的。
  • 雖然它可以防止被反射破壞,但是它確實複雜。

像上面Runtime類那樣的單例實現就差不多了,有一個缺點是,Runtime在類載入的時候就建立物件了

如果有很多類似的單例實現,在類載入時就建立了很多不需要的物件,會很佔用資源

下面寫一個懶漢式靜態內部類單例實現(呼叫時才建立物件)

public class LazyInnerClassSingleton {

    static {
System.out.println("載入靜態程式碼塊");
} private LazyInnerClassSingleton(){ System.out.println("建立物件成功"); } public static void hello(){
System.out.println("hello");
}
/*
在呼叫getInstance方法時InnerLazy類被載入的才會初始化物件
*/
public static LazyInnerClassSingleton getInstance(){
return InnerLazy.LAZY;
} private static class InnerLazy{ private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); }
}

這種實現的要點在與

  • 外部類構造方法私有化,無法建立外部類
  • 內部類的靜態變數LAZY一直到呼叫外部類的getInstance方法時才會被載入,然後LAZY物件才會被建立,實現了懶載入
  • 注意內部類只是提供例項的一個工具,這裡的單例物件是外部類

測試一下是不是真的

public class Test {

    public static void main(String[] args) throws Exception {

        LazyInnerClassSingleton.hello();
System.out.println("開始建立物件例項");
LazyInnerClassSingleton.getInstance();
}
}

由執行結果看到,它只有在呼叫getInstance方法時才會建立物件,在載入外部類時是不會載入內部類的

為了讓它不被反射破壞,在構造方法上多加一個判斷

無論是使用new關鍵字還是反射,都會呼叫類的構造方法,所以外部類使用這兩種方式字建立例項,不然就會把異常丟擲
因為if語句永遠為true,雖然在執行if語句之前,InnerLazy.LAZY為null,但是隻要使用了這個變數,就會去載入內部類
載入完內部類,InnerLazy.LAZY就不為null,於是丟擲異常

因為我沒有過破壞單例模式的經歷,所以也不知道為什麼要搞這麼複雜,只能說是很神奇。