一天一個設計模式(二) - 單例模式(Singleton)

前言

單例模式 (Singleton) 是一種建立型模式,指某個類採用Singleton模式,則在這個類被建立後,只可能產生一個實例供外部訪問,而且提供一個全局的訪問點。java

正文

(一). 優缺點

Java單例模式 (Singleton) 是一種普遍使用的設計模式。單例模式的主要做用是保證在Java程序中,某個類只有一個實例存在。一些管理器和控制器常被設計成單例模式。編程

1. 優勢

  • 提供了對惟一實例的受控訪問。
  • 因爲在系統內存中只存在一個對象,所以能夠節約系統資源,對於一些須要頻繁建立和銷燬的對象單例模式無疑能夠提升系統的性能。
  • 能夠根據實際狀況須要,在單例模式的基礎上擴展作出雙例模式,多例模式。

2. 缺點

  • 單例類的職責太重,裏面的代碼可能會過於複雜,在必定程度上違背了「單一職責原則」。
  • 若是實例化的對象長時間不被利用,會被系統認爲是垃圾而被回收,這將致使對象狀態的丟失。

(二). 具體實現

簡單點說,就是一個應用程序中,某個類的實例對象只有一個,你沒有辦法去new,由於構造器是被private修飾的,通常經過getInstance()的方法來獲取它們的實例。getInstance()的返回值是一個同一個對象的引用,並非一個新的實例。單例模式 實現起來也很容易,如下給出六種實現方式:後端

1. 餓漢式

特色:線程安全,沒法實現實例懶加載策略。設計模式

public class Singleton1 {
    private final static Singleton1 singleton1 = new  Singleton1();
    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return singleton1;
    }
}
複製代碼

2. 懶漢式

特色:線程不安全,實現了實例懶加載策略。緩存

public class Singleton2 {
    private final static Singleton2 singleton2;
    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        if (singleton2 == null)
            singleton2 = new Singleton2();
        return singleton2;
    }
}
複製代碼

3. 全局鎖式

特色:線程安全,且實現了懶加載策略,可是線程同步時效率不高。安全

public class Singleton3 {
    private final static Singleton3 singleton3;
    private Singleton3() {
    }

    public synchronized static Singleton3 getInstance() {
        if (singleton3 == null)
            singleton3 = new Singleton3();
        return singleton3;
    }
}
複製代碼

4. 靜態代碼塊式

特色:線程安全,類主動加載時才初始化實例,實現了懶加載策略,且線程安全。多線程

public class Singleton4 {
    private final static Singleton4 singleton4;
    private Singleton4() {
    }
    static {
        singleton4 = new Singleton4();
    }

    public static Singleton4 getInstance() {
        return singleton4;
    }
}
複製代碼

5. 雙重校驗鎖式

特色:線程安全,且實現了懶加載策略,同時保證了線程同步時的效率。可是volatile強制當前線程每次讀操做進行時,保證全部其餘的線程的寫操做已完成。volatile使得JVM內部的編譯器捨棄了編譯時優化,對於性能有必定的影響。架構

public class Singleton5 {
    private static volatile Singleton5 singleton5;
    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        if (singleton5 == null) {
            synchronized (Singleton5.class) {
                if (singleton5 == null) {
                    singleton5 = new Singleton5();
                }
            }
        }
        return singleton5;
    }
}
複製代碼

6. 靜態內部類式【推薦】

特色:線程安全,不存在線程同步問題,且單例對象在程序第一次 getInstance()主動加載 SingletonHolder 和其 靜態成員 INSTANCE,於是實現了懶加載策略。框架

public class Singleton6 {
    private Singleton6() {
    }

    private static class SingletonHolder {
        private static final Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return Singleton6.SingletonHolder.INSTANCE;
    }
}
複製代碼

7. 枚舉方式【做者推薦】

特色:線程安全,不存在線程同步問題,且單例對象在枚舉類型 INSTANCE 第一次引用時經過枚舉構造函數 初始化,於是實現了懶加載策略。異步

public class Singleton7 {
    private Singleton7() {
    }

    enum SingletonEnum {
        INSTANCE;

        private final Singleton7 singleton7;

        private SingletonEnum() {
            singleton7 = new Singleton7();
        }
    }

    public static Singleton7 getInstance() {
        return SingletonEnum.INSTANCE.singleton7;
    }

    public static void main(String[] args) {
        IntStream.rangeClosed(0, 100).forEach(i -> new Thread() {
            public void run() {
                out.println(Singleton7.getInstance());
            };
        }.start());
    }
}
複製代碼

這種方式是Effective Java做者 Josh Bloch 提倡的方式,它不只能避免多線程同步問題,並且還能防止反序列化從新建立新的對象,可謂是很堅強的壁壘啊。不過,因爲JDK 1.5中才加入enum特性,用這種方式寫難免讓人感受生疏。

測試代碼以下:

@FixMethodOrder
public class SingletonTester {
    protected final static int FROM = 0;
    protected final static int TO = 1000;

    protected static HashSet<Object> GLOBAL_SET = new HashSet<>();

    static {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                out.println();
                // count
                GLOBAL_SET.forEach((value) -> {
                    out.println("Global [" + value + "]");
                });
            }
        });
    }

    // testSingleton1
    @Test
    public void testSingleton1() throws Exception {
        final HashSet<Object> localSet = new HashSet<>();
        final CountDownLatch latch = new CountDownLatch(TO);
        IntStream.range(FROM, TO).forEach(i -> new Thread() {
            public void run() {
                Singleton1 singleton = Singleton1.getInstance();
                count(singleton);
            }

            protected void count(Singleton1 singleton) {
                localSet.add(singleton);
                out.println("Size of HashSet1 is: [" + localSet.size() + "]");
                // 計數減1,釋放線程
                latch.countDown();
            };
        }.start());

        // 等待子線程執行結束
        latch.await();

        synchronized (localSet) {
            // count
            localSet.forEach((value) -> {
                out.println("[" + value + "]");
                out.println();
            });
            GLOBAL_SET.addAll(localSet);
        }
    }

    // testSingleton2
    // testSingleton3
    // testSingleton4
    // testSingleton5
    // testSingleton6
    // testSingleton7
}

複製代碼

測試結果截圖以下,測試用例反映7種單例模式的方案均可以正常執行:

這裏只演示其中一種單例方式,運行截圖以下:

上圖顯示,經過 getInstance() 獲得的實例全局惟一。對於其他六中方式,根據測試用例測試獲得的結果一致,你們能夠自行測試。

總結

本文總結了七種Java中實現單例模式的方法,其中使用雙重校驗鎖靜態內部類枚舉類 的方式能夠解決大部分問題。其中,極爲推薦 靜態內部類枚舉類 這兩種實現方式。


歡迎關注技術公衆號: 零壹技術棧

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索