目錄
- 一、背景
- 二、單例模式
- 1、概念
- 2、網站計數的單例實作
- 2.1 餓漢模式
- 2.2 懶漢模式
- 2.3 列舉類實作單例模式
- 三、總結
一、背景
- 在企業網站后臺系統中,一般會將網站統計單元進行獨立設計,比如登錄人數的統計、IP數量的計數等,在這類需要完成全域統計的程序中,就會用到單例模式,即整個系統只需要擁有一個計數的全域物件,
- 在網站登錄這個高并發場景下,由這個全域物件負責統計當前網站的登錄人數、IP等,即節約了網站服務器的資源,又能保證計數的準確性,

二、單例模式
1、概念
單例模式是最常見的設計模式之一,也是整個設計模式中最簡單的模式之一,
單例模式需確保這個類只有一個實體,而且自行實體化并向整個系統提供這個實體;這個類也稱為單例類,提供全域訪問的方法,
單例模式有三大要點:
- 構造方法私有化;
-- private Singleton() { } - 實體化的變數參考私有化;
-- private static final Singleton APP_INSTANCE = new Singleton(); - 獲取實體的方法共有
-- public static SimpleSingleton getInstance() {
-- return APP_INSTANCE;
-- }
2、網站計數的單例實作
實作單例模式有多種寫法,這里我們只列舉其中最常用的三種實作方式,且考慮到網站登錄高并發場景下,將重點關注多執行緒環境下的安全問題,

- 登錄執行緒的實作
我們先創建一個登錄執行緒類,用于登錄及登錄成功后呼叫單例物件進行計數,
/**
* 單例模式的應用--登錄執行緒
*
* @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個執行緒并發登錄程序中,獲取到了同一個物件參考地址,即該單例模式是有效的,

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;
}
我們使用執行緒同步技術對懶漢式模式進行改進:
/**
* 懶漢式單例模式
*
* @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個執行緒并發登錄程序中,該單例模式是有效的,

三、總結
- 文中首先說明了單例模式在網站計數的應用:創建唯一的全域物件實作統計單元的計數,
- 根據該需求,建立了Login登錄執行緒類及App主程式,模擬多用戶同步并發登錄,
- 分別設計了餓漢模式、懶漢模式、列舉類三種不同的實作單例模式的方式,
- 在設計單例模式的程序中,特別要注意執行緒同步安全的問題,文中以懶漢模式列出了執行緒不同步的實際例子,
- 延伸思考:《Effective Java》為什么說實作單例模式的最佳方案是單元素列舉型別?
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/175702.html
標籤:Java
下一篇:常見例外型別
