1. 程式人生 > >java23種設計模式—— 二、單例模式

java23種設計模式—— 二、單例模式

原始碼在我的[github](https://github.com/witmy/JavaDesignPattern)和[gitee](https://gitee.com/witmy/JavaDesignPattern)中獲取 # 介紹 單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。 這種模式涉及到一個單一的類,該類負責建立自己的物件,同時確保只有單個物件被建立。這個類提供了一種訪問其唯一的物件的方式,可以直接訪問,不需要例項化該類的物件。 注意: - 1、單例類只能有一個例項。 - 2、單例類必須自己建立自己的唯一例項。 - 3、單例類必須給所有其他物件提供這一例項。 # 實現方式 ## 餓漢式單例(靜態常量,執行緒安全) 顧名思義,餓漢式單例它很“餓”,所以一開始就建立了唯一但單例例項,但如果你沒有使用過這個例項,就會造成記憶體的浪費 ```java /** * 餓漢式單例 * 優點:簡單,在類裝載時就完成了例項化,避免了執行緒同步問題,執行緒安全 * 缺點:由於這個類已經完成了例項化,如果從始至終都沒有用過這個例項,就會造成記憶體的浪費 */ public class SingletonTest01 { public static void main(String[] args) { Signleton instance1= Signleton.getInstance(); Signleton instance2 = Signleton.getInstance(); System.out.println(instance1==instance2); System.out.println(instance1.hashCode()); System.out.println(instance2.hashCode()); } } class Signleton{ //1、構造器私有化,外部無法通過new新建 private Signleton(){ } //2、內部建立物件例項 private final static Signleton instance = new Signleton(); //3、提供一個公有的靜態方法,返回例項物件 public final static Signleton getInstance(){ return instance; } } ``` 輸出結果 ```bash true 1163157884 1163157884 ``` 可以看到輸出的是同一個例項 ## 餓漢式單例(靜態程式碼塊,執行緒安全) 和之前的方式類似,只不過將類例項化的過程放在了靜態程式碼塊中,也就是類裝載的時候, 就執行靜態程式碼塊中的程式碼,優缺點和之前一樣 ```java /** * 和之前的方式類似,只不過將類例項化的過程放在了靜態程式碼塊中,也就是類裝載的時候, * 就執行靜態程式碼塊中的程式碼,優缺點和之前一樣 */ public class SingletonTest02 extends Thread{ public static void main(String[] args) { Signleton instance1= Signleton.getInstance(); Signleton instance2 = Signleton.getInstance(); System.out.println(instance1==instance2); System.out.println(instance1.hashCode()); System.out.println(instance2.hashCode()); } } class Signleton{ //1、構造器私有化,外部無法通過new新建 private Signleton(){} //2、內部建立物件例項 private static Signleton instance; static {//靜態程式碼塊種,建立單例物件 instance = new Signleton(); } //3、提供一個公有的靜態方法,返回例項物件 public final static Signleton getInstance(){ return instance; } } ``` 輸出 ```bash true 1163157884 1163157884 ``` ## 懶漢式(執行緒不安全) 同樣,顧名思義,懶漢式單例它很懶。只有在你用到它時,它才會建立一個例項。 ```java /** * 餓漢式-執行緒不安全 * 優點:起到了懶載入的效果,但是隻能在單執行緒下使用 * 如果在多執行緒下,如果一個執行緒進入了if判斷語句塊, * 還沒來得及向下執行,另一個執行緒也進入這個判斷語句,就會產生多個例項(違背單例模式), * 實際開發中,不要使用這種方式 */ public class SingletonTest03 { public static void main(String[] args) { for (int i = 0; i <10 ; i++) { new Thread(() -> System.out.println(Signleton.getInstance().hashCode()) ).start(); } } } class Signleton{ private static Signleton instance; private Signleton(){} //提供一個靜態的公有方法,當呼叫方法時,才去建立instance public static Signleton getInstance(){ if(instance == null){//如果為空再去建立物件 instance = new Signleton(); } return instance; } } ``` 輸出 ```bash 546405844 135417039 135417039 802181073 135417039 135417039 135417039 802181073 135417039 135417039 ``` 這裡我選了個比較極端的情況,如果你的電腦配置比較好,可能執行幾次結果都是符合單例模式的。 ## 懶漢式(同步方法,執行緒安全) 上面方法之所以會存線上程不安全的情況,是因為多執行緒情況下,可能會有多條執行緒同時判斷單例是否建立。那麼要解決這個問題 ,只需要同步getInstance()方法 ```java /** * 解決了執行緒不安全的問題 * 但是大大降低了效率 每個執行緒想獲得例項的時候,執行getInstance()方法都要進行同步 */ public class SingletonTest04 { public static void main(String[] args) { for (int i = 0; i <10 ; i++) { new Thread(() -> System.out.println(Signleton.getInstance().hashCode()) ).start(); } } } class Signleton{ private static Signleton instance; private Signleton(){} //提供一個靜態的公有方法,當呼叫方法時,才去建立instance public static synchronized Signleton getInstance(){ if(instance == null){//如果為空再去建立物件 instance = new Signleton(); } return instance; } } ``` 結果 ```bash 802181073 802181073 802181073 802181073 802181073 802181073 802181073 802181073 802181073 802181073 ``` 但是,synchronized是一個很重量的同步鎖,而我們每次執行getInstance()時都會進行同步,極其影響效率 ## 懶漢式(雙重檢查,執行緒安全) 雙檢鎖,又叫雙重校驗鎖,綜合了懶漢式和餓漢式兩者的優缺點整合而成。看上面程式碼實現中,特點是在synchronized關鍵字內外都加了一層 if 條件判斷,這樣既保證了執行緒安全,又比直接上鎖提高了執行效率,還節省了記憶體空間 ```java /** * 懶漢模式-雙重檢查 * 進行了兩次if判斷檢查,這樣就保證執行緒安全了 * 通過判斷是否為空,來確定是否 需要再次例項化 */ public class SingletonTest05 { public static void main(String[] args) { for (int i = 0; i <10 ; i++) { new Thread(() -> System.out.println(Signleton.getInstance().hashCode()) ).start(); } } } class Signleton{ private static volatile Signleton instance;//volatile保證可見性 private Signleton(){} //提供一個靜態的公有方法,加入雙重檢查程式碼,解決執行緒安全問題,同時解決懶載入問題 public static Signleton getInstance() { if (instance == null) { synchronized (Signleton.class) { if (instance == null) { instance = new Signleton(); } } } return instance; } } ``` 執行結果 ```bash 79372097 79372097 79372097 79372097 79372097 79372097 79372097 79372097 79372097 79372097 ``` 推薦使用 ## 靜態內部類(執行緒安全) ```java /** * 靜態內部類實現單例模式 * 該方法採用了類裝載機制來保證初始化例項時只有一個執行緒 * 靜態內部類在Signleton類被裝載時並不會立即例項化,而是需要例項化時,才會裝載SignletonInstance類 * 類的靜態屬性只會在第一次載入類的時候初始化 * 避免了執行緒不安全,利用靜態內部類實現懶載入,效率高 */ public class SingletonTest07 { public static void main(String[] args) { for (int i = 0; i <10 ; i++) { new Thread(() -> System.out.println(Signleton.getInstance().hashCode()) ).start(); } } } class Signleton{ //構造器私有 private Signleton(){} //靜態內部類,該類中有一個靜態屬性Signleton private static class SignletonInstance{ private static final Signleton instance = new Signleton(); } //提供一個靜態的公有方法,直接返回SignletonInstance.instance public static Signleton getInstance() { return SignletonInstance.instance; } } ``` 結果 ```bash 79372097 79372097 79372097 79372097 79372097 79372097 79372097 79372097 79372097 79372097 ``` 這種方式較為簡單,推薦使用 ## 列舉(執行緒安全) ```java /** * @author codermy * @createTime 2020/5/14 * 列舉方法實現單例模式 * 藉助jdk1.5中新增的列舉類來實現單例模式, * 不僅能避免多執行緒同步問題,而且還能防止反序列化重新建立新物件 */ public class SingletonTest08 { public static void main(String[] args) { Singleton singleton = Singleton.INSTANCE; singleton.Ok(); for (int i = 0; i <10 ; i++) { new Thread(() -> System.out.println(Singleton.INSTANCE.hashCode()) ).start(); } } } enum Singleton{ INSTANCE;//屬性 public void Ok(){ System.out.println("ok"); } } ``` 結果 ```bash ok 858497792 858497792 858497792 858497792 858497792 858497792 858497792 858497792 858497792 858497792 ``` 可以看出,列舉實現單例模式,最為簡潔,較為推薦。但是正是因為它簡潔,導致可讀