用一個通俗易懂的實戰案例,完全搞懂單例模式

1、背景

  • 在企業網站後臺系統中,通常會將網站統計單元進行獨立設計,好比登陸人數的統計、IP數量的計數等。在這類須要完成全局統計的過程當中,就會用到單例模式,即整個系統只須要擁有一個計數的全局對象。
  • 在網站登陸這個高併發場景下,由這個全局對象負責統計當前網站的登陸人數、IP等,即節約了網站服務器的資源,又能保證計數的準確性。

用一個通俗易懂的實戰案例,完全搞懂單例模式

2、單例模式

一、概念

單例模式是最多見的設計模式之一,也是整個設計模式中最簡單的模式之一。設計模式

單例模式需確保這個類只有一個實例,並且自行實例化並向整個系統提供這個實例;這個類也稱爲單例類,提供全局訪問的方法。安全

單例模式有三大要點:服務器

  • 構造方法私有化;
    -- private Singleton() { }
  • 實例化的變量引用私有化;
    -- private static final Singleton APP_INSTANCE = new Singleton();
  • 獲取實例的方法共有
    -- public static SimpleSingleton getInstance() {
    -- return APP_INSTANCE;
    -- }

二、網站計數的單例實現

實現單例模式有多種寫法,這裏咱們只列舉其中最經常使用的三種實現方式,且考慮到網站登陸高併發場景下,將重點關注多線程環境下的安全問題。多線程

用一個通俗易懂的實戰案例,完全搞懂單例模式

  • 登陸線程的實現
    咱們先建立一個登陸線程類,用於登陸及登陸成功後調用單例對象進行計數。
/**
 * 單例模式的應用--登陸線程
 *
 * @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個用戶併發登陸,完成登陸後輸出登陸人次計數。
/**
 * 單例模式--主程序
 *
 * @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 餓漢模式
  • 在程序啓動之初就進行建立( 無論三七二十一,先建立出來再說)。
  • 天生的線程安全。
  • 不管程序中是否用到該單例類都會存在。
/**
 * 餓漢式單例模式
 *
 * @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

用一個通俗易懂的實戰案例,完全搞懂單例模式

2.2 懶漢模式

  • 在初始化時只進行定義。
  • 只有在程序中調用了該單例類,纔會完成實例化( 沒人動我,我才懶得動)。
  • 需經過線程同步技術才能保證線程安全。

咱們先看下未使用線程同步技術的例子:高併發

/**
 * 懶漢式單例模式--未應用線程同步技術
 *
 * @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個線程併發登陸過程當中,獲取到了同一個對象引用地址,即該單例模式有效了。

用一個通俗易懂的實戰案例,完全搞懂單例模式

2.3 枚舉類實現單例模式

《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個線程併發登陸過程當中,該單例模式是有效的。

用一個通俗易懂的實戰案例,完全搞懂單例模式

3、總結

  1. 文中首先說明了單例模式在網站計數的應用:建立惟一的全局對象實現統計單元的計數。
  2. 根據該需求,創建了Login登陸線程類及App主程序,模擬多用戶同步併發登陸。
  3. 分別設計了餓漢模式、懶漢模式、枚舉類三種不一樣的實現單例模式的方式。
  4. 在設計單例模式的過程當中,特別要注意線程同步安全的問題,文中以懶漢模式列出了線程不一樣步的實際例子。
  5. 延伸思考:《Effective Java》爲何說實現單例模式的最佳方案是單元素枚舉類型?
相關文章
相關標籤/搜索