單例模式是最多見的設計模式之一,也是整個設計模式中最簡單的模式之一。設計模式
單例模式需確保這個類只有一個實例,並且自行實例化並向整個系統提供這個實例;這個類也稱爲單例類,提供全局訪問的方法。安全
單例模式有三大要點:服務器
實現單例模式有多種寫法,這裏咱們只列舉其中最經常使用的三種實現方式,且考慮到網站登陸高併發場景下,將重點關注多線程環境下的安全問題。多線程
/** * 單例模式的應用--登陸線程 * * @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 // 登陸成功後調用單例對象進行計數 } }
/** * 單例模式--主程序 * * @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 // 調用單例對象輸出登陸人數統計 }
/** * 餓漢式單例模式 * * @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); } }
咱們將餓漢模式的單例對象加入進登陸線程及主程序中進行測試:併發
/** * 單例模式的應用--登陸線程 * * @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個線程併發登陸過程當中,獲取到了同一個對象引用地址,即該單例模式是有效的。ide
咱們先看下未使用線程同步技術的例子:高併發
/** * 懶漢式單例模式--未應用線程同步技術 * * @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); } }
/** * 單例模式的應用--登陸線程 * * @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個線程併發登陸過程當中,獲取到了四個對象引用地址,該單例模式失效了。測試
對代碼進行分析:網站
// 未使用線程同步 public static LazySingleton getInstance() { // 在多個線程併發時,可能會有多個線程同時進入 if 語句,致使產生多個實例 if (APP_INSTANCE == null) { APP_INSTANCE = new LazySingleton(); } return APP_INSTANCE; }
咱們使用線程同步技術對懶漢式模式進行改進:ui
/** * 懶漢式單例模式 * * @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個線程併發登陸過程當中,獲取到了同一個對象引用地址,即該單例模式有效了。
《Effective 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); } }
/** * 單例模式的應用--登陸線程 * * @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個線程併發登陸過程當中,該單例模式是有效的。