(一)需求場景
程序在運行的時候,通過會生成很多的實例,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() {}
}