1. 程式人生 > >用一個通俗易懂的例子徹底說清楚單例模式

用一個通俗易懂的例子徹底說清楚單例模式

[toc] #### 一、背景 - 在企業網站後臺系統中,一般會將網站統計單元進行獨立設計,比如登入人數的統計、IP數量的計數等。在這類需要完成全域性統計的過程中,就會用到**單例模式**,即整個系統只需要擁有一個計數的全域性物件。 - 在網站登入這個高併發場景下,由這個全域性物件負責統計當前網站的登入人數、IP等,即節約了網站伺服器的資源,又能保證計數的準確性。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200601152935409.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pwZ3podQ==,size_16,color_FFFFFF,t_70) #### 二、單例模式 ##### 1、概念 單例模式是最常見的設計模式之一,也是整個設計模式中最簡單的模式之一。 > **單例模式需確保這個類只有一個例項,而且自行例項化並向整個系統提供這個例項;這個類也稱為單例類,提供全域性訪問的方法。** 單例模式有三大要點: - 構造方法私有化; -- *private Singleton() { }* - 例項化的變數引用私有化; -- *private static final Singleton APP_INSTANCE = new Singleton();* - 獲取例項的方法共有 -- *public static SimpleSingleton getInstance() {* -- *return APP_INSTANCE; -- }* ##### 2、網站計數的單例實現 實現單例模式有多種寫法,這裡我們只列舉其中最常用的三種實現方式,且考慮到網站登入高併發場景下,將重點關注多執行緒環境下的安全問題。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200601162249438.png) - 登入執行緒的實現 我們先建立一個登入執行緒類,用於登入及登入成功後呼叫單例物件進行計數。 ```java /** * 單例模式的應用--登入執行緒 * * @author zhuhuix * @date 2020-06-01 */ public class Login implements Runnable { // 登入名稱 private String loginName; public String getLoginName() { return loginName; } public void setLoginName(String loginName) { this.loginName = loginName; } @Override public void run() { // TODO // 登入成功後呼叫單例物件進行計數 } } ``` - 主程式的實現 編寫一個主程式,利用多執行緒技術模擬10個使用者併發登入,完成登入後輸出登入人次計數。 ```java /** * 單例模式--主程式 * * @author zhuhuix * @date 2020-06-01 */ public class App { public final static int num = 10; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[num]; for (int i = 0; i < num; i++) { Login login = new Login(); login.setLoginName("" + String.format("%2s", (i + 1)) + "號使用者"); threads[i] = new Thread(login); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } // TODO // 呼叫單例物件輸出登入人數統計 } ``` ###### 2.1 餓漢模式 - 在程式啟動之初就進行建立( 不管三七二十一,先創建出來再說)。 - 天生的執行緒安全。 - 無論程式中是否用到該單例類都會存在。 ```java /** * 餓漢式單例模式 * * @author zhuhuix * @date 2020-06-01 */ public class SimpleSingleton implements Serializable { // 單例物件 private static final SimpleSingleton APP_INSTANCE = new SimpleSingleton(); // 計數器 private AtomicLong count = new AtomicLong(0); // 單例模式必須保證預設構造方法為私有型別 private SimpleSingleton() { } public static SimpleSingleton getInstance() { return APP_INSTANCE; } public AtomicLong getCount() { return count; } public void setCount() { count.addAndGet(1); } } ``` 我們將餓漢模式的單例物件加入進登入執行緒及主程式中進行測試: ```java /** * 單例模式的應用--登入執行緒 * * @author zhuhuix * @date 2020-06-01 */ public class Login implements Runnable { // 登入名稱 private String loginName; public String getLoginName() { return loginName; } public void setLoginName(String loginName) { this.loginName = loginName; } @Override public void run() { // 餓漢式單例 SimpleSingleton simpleSingleton= SimpleSingleton.getInstance(); simpleSingleton.setCount(); System.out.println(getLoginName()+"登入成功:"+simpleSingleton.toString()); } } /** * 單例模式--主程式 * * @author zhuhuix * @date 2020-06-01 */ public class App { public final static int num = 10; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[num]; for (int i = 0; i < num; i++) { Login login = new Login(); login.setLoginName("" + String.format("%2s", (i + 1)) + "號使用者"); threads[i] = new Thread(login); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println("網站共有"+SimpleSingleton.getInstance().getCount()+"個使用者登入"); } } ``` 輸出如下: **10個執行緒併發登入過程中,獲取到了同一個物件引用地址,即該單例模式是有效的。** ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200601163338159.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pwZ3podQ==,size_16,color_FFFFFF,t_70) ###### 2.2 懶漢模式 - 在初始化時只進行定義。 - 只有在程式中呼叫了該單例類,才會完成例項化( 沒人動我,我才懶得動)。 - 需通過執行緒同步技術才能保證執行緒安全。 我們先看下未使用執行緒同步技術的例子: ```java /** * 懶漢式單例模式--未應用執行緒同步技術 * * @author zhuhuix * @date 2020-06-01 */ public class LazySingleton { // 單例物件 private static LazySingleton APP_INSTANCE; // 計數器 private AtomicLong count = new AtomicLong(0); // 單例模式必須保證預設構造方法為私有型別 private LazySingleton() { } public static LazySingleton getInstance() { if (APP_INSTANCE == null) { APP_INSTANCE = new LazySingleton(); } return APP_INSTANCE; } public AtomicLong getCount() { return count; } public void setCount() { count.addAndGet(1); } } ``` ```java /** * 單例模式的應用--登入執行緒 * * @author zhuhuix * @date 2020-06-01 */ public class Login implements Runnable { .... @Override public void run() { // 餓漢式單例 LazySingleton lazySingleton =LazySingleton.getInstance(); lazySingleton.setCount(); System.out.println(getLoginName()+"登入成功:"+lazySingleton); } } /** * 單例模式--主程式- * * @author zhuhuix * @date 2020-06-01 */ public class App { public final static int num = 10; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[num]; for (int i = 0; i < num; i++) { Login login = new Login(); login.setLoginName("" + String.format("%2s", (i + 1)) + "號使用者"); threads[i] = new Thread(login); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println("網站共有" + LazySingleton.getInstance().getCount() + "個使用者登入"); } } ``` 輸出結果: **10個執行緒併發登入過程中,獲取到了四個物件引用地址,該單例模式失效了。** ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200601164919263.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pwZ3podQ==,size_16,color_FFFFFF,t_70) 對程式碼進行分析: ```java // 未使用執行緒同步 public static LazySingleton getInstance() { // 在多個執行緒併發時,可能會有多個執行緒同時進入 if 語句,導致產生多個例項 if (APP_INSTANCE == null) { APP_INSTANCE = new LazySingleton(); } return APP_INSTANCE; } ``` 我們使用執行緒同步技術對懶漢式模式進行改進: ```java /** * 懶漢式單例模式 * * @author zhuhuix * @date 2020-06-01 */ public class LazySingleton { // 單例物件 ,加入volatile關鍵字進行修飾 private static volatile LazySingleton APP_INSTANCE; // 計數器 private AtomicLong count = new AtomicLong(0); // 單例模式必須保證預設構造方法為私有型別 private LazySingleton() { } public static LazySingleton getInstance() { if (APP_INSTANCE == null) { // 對類進行加鎖,並進行雙重檢查 synchronized (LazySingleton.class) { if (APP_INSTANCE == null) { APP_INSTANCE = new LazySingleton(); } } } return APP_INSTANCE; } public AtomicLong getCount() { return count; } public void setCount() { count.addAndGet(1); } } ``` 再測試執行: **10個執行緒併發登入過程中,獲取到了同一個物件引用地址,即該單例模式有效了。** ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020060116571577.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pwZ3podQ==,size_16,color_FFFFFF,t_70) ###### 2.3 列舉類實現單例模式 > **《Effective Java》 推薦使用列舉的方式解決單例模式。這種方式解決了最主要的;執行緒安全、自由序列化、單一例項。** ```java /** * 利用列舉類實現單例模式 * * @author zhuhuix * @date 2020-06-01 */ public enum EnumSingleton implements Serializable { // 單例物件 APP_INSTANCE; // 計數器 private AtomicLong count = new AtomicLong(0); // 單例模式必須保證預設構造方法為私有型別 private EnumSingleton() { } public AtomicLong getCount() { return count; } public void setCount() { count.addAndGet(1); } } ``` ```java /** * 單例模式的應用--登入執行緒 * * @author zhuhuix * @date 2020-06-01 */ public class Login implements Runnable { ... @Override public void run() { EnumSingleton enumSingleton = EnumSingleton.APP_INSTANCE; enumSingleton.setCount(); System.out.println(getLoginName()+"登入成功:"+enumSingleton.toString()); } } /** * 單例模式--主程式 * * @author zhuhuix * @date 2020-06-01 */ public class App { public final static int num = 10; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[num]; for (int i = 0; i < num; i++) { Login login = new Login(); login.setLoginName("" + String.format("%2s", (i + 1)) + "號使用者"); threads[i] = new Thread(login); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println("網站共有"+EnumSingleton.APP_INSTANCE.getCount()+"個使用者登入"); } } ``` 輸出如下: **10個執行緒併發登入過程中,該單例模式是有效的。** ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200601171850749.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pwZ3podQ==,size_16,color_FFFFFF,t_70) #### 三、總結 1. 文中首先說明了單例模式在網站計數的應用:建立唯一的全域性物件實現統計單元的計數。 2. 根據該需求,建立了Login登入執行緒類及App主程式,模擬多使用者同步併發登入。 3. 分別設計了餓漢模式、懶漢模式、列舉類三種不同的實現單例模式的方式。 4. 在設計單例模式的過程中,特別要注意執行緒同步安全的問題,文中以懶漢模式列出了執行緒不同步的實際例子。 5. *延伸思考*:《Effective Java》為什麼說實現單例模式的最佳方案是單元素列舉