單例模式的使用
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,於是丟擲異常