單例模式

(一)需求場景

程序在運行的時候,通過會生成很多的實例,String類實例,當有N個字符串的時候,就會創建N個實例.但是我們在編寫程序的時候不一定都需要重新創建新的實例,或許更需要“只創建一個實例”的情況,這也更能減少資源消耗.

(二)基本介紹

單例模式(Singleton Pattern)正是完成這種“只存在一個實例”的模式,這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。

這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象

(三)工作原理

1:確保在任何情況下絕對只有一個實例(尤其是在高併發多線程情況下)
2:私有化構造器,禁止外部創建新的對象
3:定義Static字段的私有類成員變量,該成員就是唯一存在的實例對象
4:提供公共的靜態對外訪問方法,返回唯一的實例
5:構建單例模式大約有八種,這裏列舉了幾種參考,其中實際開發中建議使用雙重檢驗鎖方式

(四)餓漢模式

最常見、最簡單的單例模式寫法之一。顧名思義,“餓漢模式” 就是很 “飢渴”,所以一上來就需要給它新建一個實例。但這種方法有一個明顯的缺點,那就是不管有沒有調用過獲得實例的方法,每次都會新建一個實例

public class Singleton {

    private static final Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return singleton;
    }
}
(五)懶漢模式

顧名思義,“懶漢模式” 就是它很懶,一開始不新建實例,只有當它需要使用的時候,會先判斷實例是否爲空,如果爲空纔會新建一個實例來使用。

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
(六)線程安全的懶漢模式

上面的懶漢模式存在一個嚴重的問題。那就是如果有多個線程並行調用獲取實例方法的時候,還是會創建多個實例,因此我們需要在基本的懶漢模式上,把它設爲線程同步(synchronized)就好了。synchronized 的作用就是保證在同一時刻最多隻有一個線程運行,這樣就避免了多線程帶來的問題。(備註:可以使用同步代碼塊也可以使用同步方法)

//同步方法
public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

    public static synchronized Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
//同步代碼塊
public class Singleton {
    private static Singleton singleton;
    private Singleton() {
    }
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}
(七)雙重檢驗鎖(double check)方式

線程安全的懶漢模式解決了多線程的問題。但是它的效率不高,每次調用獲得實例的方法時都要進行同步,但是多數情況下並不需要同步操作加同步方法.所以只需要在第一次新建實例對象的時候,使用同步方法.經過改進,有了如下的雙重檢驗鎖方式。同時注意一個問題,成員變量可能也會存在線程併發問題.因爲在 JVM執行這句代碼的時候,JVM 爲了優化代碼,有可能造成做這幾件事情的執行順序是不固定的,從而造成錯誤(指令重排序)。
這個時候,我們需要給實例加一個 volatile 關鍵字,它的作用就是防止編譯器自行優化代碼,禁止指令重排。

public class Singleton {
    private volatile  static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        // 第一個檢驗鎖,如果不爲空直接返回實例對象,爲空才進入下一步
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    //第二個檢驗鎖,因爲可能有多個線程進入到 if 語句內
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
(八)靜態內部類方式

volatile 在老版本的JDK中無法使用,我們還可以使用靜態內部類的方式來創建單例。這種方式採用了類裝載的機制來保證初始化實例時只有一個線程。靜態內部類方式在Singleton類被裝載時並不會立即實例化,而是在需要實例化時,調用getInstance方法,纔會裝載SingletonInstance類,從而完成Singleton的實例化。類的靜態屬性只會在第一次加載類的時候初始化,所以在這裏,JVM幫助我們保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。避免了線程不安全,利用靜態內部類特點實現延遲加載,效率高.

public class Singleton {
    private static class InnerSingleton {
        private final static Singleton singleton = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return InnerSingleton.singleton;
    }
}
(九)枚舉方式

由於Java通過Java反射機制是能夠實例化構造方法爲private的類的,因此上述的方式其實都是可以通過反射創建多個實例,這樣也就造成線程不安全.針對這種情況,我們可以使用枚舉,枚舉類型是無法通過反射進行實例化的,而且枚舉返回的實例都是單例的.
備註:,枚舉方式是Effective Java作者Josh Bloch 提倡的方式

public enum Singleton {
    INSTANCE;
     //省略成員變量和方法
}
public static void main(String[] args){
      //獲取實例   
 Singleton singleton = Singleton.INSTANCE;
        
    }

擴展:證明反射無法反射枚舉類型

 public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<Singleton> singletonClass = Singleton.class;
        Constructor<?>[] constructors = singletonClass.getDeclaredConstructors();
        for (Constructor<?> c : constructors) {
            //允許訪問私有
            c.setAccessible(true);
            //反射創建實例
            Object instance = c.newInstance();
        }
    }

在這裏插入圖片描述
當反射枚舉類獲取實例對象時報錯,報錯原因參見下方截圖,無法反射枚舉對象
在這裏插入圖片描述

(十)應用實例

JDK中 java.lang.Runtime就是經典的單例模式(餓漢式)

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

 
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

}	
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章