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