設計模式:單例模式的使用和實現(JAVA)


單例模式的使用

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,於是拋出異常


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM