摘要
本文詳細介紹了單例設計模式,包括其定義、結構、實現方法及適用場景。單例模式是一種創建型設計模式,確保一個類只有一個實例并提供全局訪問點。其要點包括唯一性、私有構造函數、全局訪問點和線程安全。文章還展示了單例設計模式的類圖和時序圖,并介紹了三種實現方式:餓漢式、靜態內部類和枚舉方式。最后列舉了單例模式適合和不適合的場景,以及實戰建議和示例,如配置中心、統一 ID 生成器、日志收集器等。
1. 單例設計模式定義
單例模式是一種創建型設計模式,其目的是確保一個類只有一個實例,并提供一個全局訪問點來獲取該實例。
通俗理解:
- 單例模式就是讓一個類只創建一個對象,就像系統中只能有一個“總統”或“日志管理器”。
- 這個類自己控制這個唯一實例的創建,并且其他類只能通過它提供的方法來獲取這個對象。
要點 | 說明 |
唯一性 | 類只能有一個實例 |
私有構造函數 | 禁止外部直接用 |
全局訪問點 | 提供一個靜態方法獲取該實例 |
線程安全(可選) | 在多線程環境下仍能保持唯一性 |
2. 單例設計模式結構
2.1. 單例設計模式類圖
2.2. 單例設計模式時序圖
3. 單例設計模式實現方式
所有單例的實現都包含以下兩個相同的步驟:
- 將默認構造函數設為私有, 防止其他對象使用單例類的
new
運算符。 - 新建一個靜態構建方法作為構造函數。 該函數會 “偷偷” 調用私有構造函數來創建對象, 并將其保存在一個靜態成員變量中。 此后所有對于該函數的調用都將返回這一緩存對象。
如果你的代碼能夠訪問單例類, 那它就能調用單例類的靜態方法。 無論何時調用該方法, 它總是會返回相同的對象。
實現方式 | 是否線程安全 | 是否懶加載 | 推薦程度 |
餓漢式 | 是 | 否 | ? 推薦(簡單可靠) |
懶漢式(線程不安全) | 否 | 是 | ? 不推薦 |
懶漢式 + synchronized | 是 | 是 | ?? 有性能開銷 |
雙重檢查鎖(DCL) | 是 | 是 | ? 推薦(兼顧性能) |
靜態內部類 | 是 | 是 | ? 推薦(懶加載 + 安全) |
枚舉方式 | 是 | 否 | ? 最推薦(防反射、反序列化) |
3.1. 📍 餓漢式(推薦)
public class Singleton {private static final Singleton instance = new Singleton();private Singleton() {} // 構造器私有化public static Singleton getInstance() {return instance;}
}
3.2. 📍 靜態內部類(推薦)
public class Singleton {private Singleton() {}private static class Holder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return Holder.INSTANCE;}
}
3.3. 📍 枚舉方式(最安全)
public enum Singleton {INSTANCE;public void doSomething() {System.out.println("do...");}
}
4. 單例設計模式適合場景
4.1. ? 單例模式適合的場景
場景類別 | 說明 | 示例 |
配置管理類 | 系統中讀取一次后多處使用,需全局共享 |
|
日志系統 | 全局統一記錄日志,防止多個文件或實例導致管理混亂 |
|
線程池 / 連接池 | 統一管理資源,避免重復創建、浪費連接 |
|
任務調度器 | 控制任務執行的唯一調度入口 |
|
唯一 ID 生成器 | 全局 ID 要求唯一,需中心化生成 |
|
系統監控模塊 | 全局收集監控信息,避免多個統計點造成數據不一致 |
|
4.2. ? 單例模式不適合的場景
場景類別 | 問題描述 | 示例或說明 |
會話/用戶狀態類 | 多用戶或請求需獨立狀態,單例可能造成狀態串擾或數據混亂。 | 用戶登錄狀態、購物車信息等 |
多實例業務模型 | 業務本身設計要求一個類存在多個不同實例 | 訂單、交易、商品等 |
單元測試場景 | 單例難以隔離狀態,不利于并發測試和 mock。 | 單例殘留狀態會污染其他測試 |
生命周期綁定業務對象 | 對象需按請求或事務創建銷毀,單例不符合生命周期需求。 | HTTP 請求上下文、數據庫事務上下文 |
狀態頻繁變化類 | 狀態共享會導致線程不安全,需加鎖處理復雜性上升。 | 非線程安全的緩存組件、計算任務執行狀態 |
需依賴注入管理的類 | 單例可能和 Spring 等框架的容器管理沖突,影響可測試性和解耦性。 | 建議用 Spring Bean 單例管理( |
4.3. 🧠 單例設計模式實戰建議
使用場景 | 是否推薦使用單例 | 說明 |
配置類 / 常量類 | ? 是 | 全局唯一即可 |
Controller/Service | ? 否 | 由 Spring 容器管理生命周期更合適 |
每個用戶/請求有狀態 | ? 否 | 應使用原型模式或線程隔離 |
工具類(無狀態) | ?? 視情況 | 可以用 |
5. 單例設計模式實戰示例
在 Spring 項目中,單例模式(Singleton Pattern)使用場景非常廣泛。Spring 容器管理的 Bean 默認就是單例模式,它本質上滿足了單例設計模式的定義:“確保一個類只有一個實例,并且提供一個全局訪問點。所以在 Spring 項目中,我們一般直接使用 Spring 單例 Bean,既符合單例設計模式的定義,又簡化了開發和維護的復雜度。下面是一些常見 適合使用單例的場景 及其在 Spring 中的實現示例:
設計模式核心要求 | Spring Bean 示例體現方式 |
唯一實例(Singleton) | Spring 容器中該 Bean 默認只實例化一次,所有注入該 Bean 的地方都共享同一個實例。 |
全局訪問點 | 通過 Spring 的依賴注入( |
控制實例創建(防止多次 new) | 不用 |
線程安全(視具體實現而定) | 需要保證成員變量線程安全,比如用線程安全的數據結構或無狀態設計。 |
5.1. ? 配置中心 / 配置管理器
使用場景: 需要在系統中讀取一次配置,供全局使用。
@Component
@Data
public class AppConfig {@Value("${app.env}")private String env;
}
使用方式:
@Service
public class MyService {@Autowiredprivate AppConfig appConfig;public void doSomething() {System.out.println(appConfig.getEnv());}
}
5.2. ? 統一 ID 生成器(如雪花算法)
@Component
public class IdGenerator {private final AtomicLong counter = new AtomicLong();public long nextId() {return counter.incrementAndGet();}
}
使用方式:
@Service
public class OrderService {@Autowiredprivate IdGenerator idGenerator;public void createOrder() {Long orderId = idGenerator.nextId();// 創建訂單邏輯}
}
5.3. ? 日志收集器 / 監控埋點上報器
@Component
public class MetricsCollector {public void record(String metric, int value) {// 上報指標邏輯System.out.println("metric: " + metric + " value: " + value);}
}
使用方式:
@Service
public class PaymentService {@Autowiredprivate MetricsCollector metricsCollector;public void pay() {// 業務邏輯metricsCollector.record("payment.count", 1);}
}
5.4. ? 緩存組件(輕量場景)
@Component
public class LocalCache {private final Map<String, Object> cache = new ConcurrentHashMap<>();public void put(String key, Object value) {cache.put(key, value);}public Object get(String key) {return cache.get(key);}
}
5.5. ? 線程池 / 異步任務執行器(通過 Spring 管理)
@Configuration
public class ThreadPoolConfig {@Beanpublic Executor taskExecutor() {return Executors.newFixedThreadPool(10);}
}
使用方式:
@Service
public class AsyncTaskService {@Autowiredprivate Executor taskExecutor;public void runAsyncTask() {taskExecutor.execute(() -> System.out.println("Running async task"));}
}
5.6. ? 策略工廠 / 狀態機容器
這些模式本質上也是通過單例注冊機制實現的,通常用 @Component
+ Map<String, Strategy>
組合來做策略路由。
@Component
public class StrategyFactory {private final Map<String, Strategy> strategies;public StrategyFactory(List<Strategy> strategyList) {strategies = new HashMap<>();for (Strategy s : strategyList) {strategies.put(s.getType(), s);}}public Strategy get(String type) {return strategies.get(type);}
}
5.7. ? Spring 項目中適合單例的場景
場景名稱 | Spring 推薦實現 | 是否線程安全 |
配置類 |
| ? 是 |
ID 生成器 |
| ? 是 |
日志/監控工具 |
| ? 是 |
緩存組件 |
| ? 是(注意并發) |
工具類 |
| ?? 視情況 |
6. 單例設計模式思考
6.1. 為什么spring中對象天然是單例?
6.1.1. Spring 容器設計初衷
- Spring 是一個IoC(控制反轉)容器,負責管理應用中的對象生命周期和依賴關系。
- 容器初始化時,會根據配置(注解或 XML)創建并管理 Bean 實例。
- 默認情況下,Spring 容器只會創建一個共享的 Bean 實例,供所有依賴該 Bean 的組件共享使用。
6.1.2. 單例 Bean 的定義和作用域
- Spring 中的單例是指在 Spring 容器中只有一個實例,而不是 JVM 層面上的全局單例。
- 默認作用域是
singleton
,即:每個 Spring 容器中該 Bean 只有一個實例
- 你可以通過
@Scope("prototype")
等其他作用域來改變默認行為。
6.1.3. Spring 單例實現機制(簡要)
- 容器啟動時,會掃描并實例化所有單例 Bean。
- 創建后,將實例放入一個單例緩存池(例如
singletonObjects
)。 - 當其他組件請求該 Bean 時,直接從緩存池取,避免重復創建。
- 通過這種方式,Spring 確保每個 Bean 在容器內是唯一的。
6.1.4. 為什么默認使用單例?
- 節省資源:不必每次調用都創建新實例,減少內存開銷。
- 方便共享:多個組件可以共享狀態或行為一致的對象。
- 生命周期管理:由容器統一管理,便于統一銷毀或初始化。
- 線程安全的前提下,提高性能:一般單例 Bean 設計為無狀態或線程安全,避免多次實例化開銷。
6.1.5. 需要注意的點
- Spring 的單例是“容器單例”,不同的 Spring 容器可以有不同的實例。
- 如果使用多個容器或類加載器,則可能出現多實例。
- 單例 Bean 設計時應注意線程安全,避免可變狀態帶來的并發問題。
- 業務中有狀態的 Bean 一般不要用單例,使用
prototype
或其他作用域。
6.2. Spring 的單例是“容器單例”,不同的 Spring 容器可以有不同的實例。
意思是: Spring 單例不是 JVM 層面全局的單例,而是“每個 Spring 容器(ApplicationContext)中唯一的實例”。如果你項目里啟動了多個 Spring 容器(比如多個 ApplicationContext
實例),每個容器都會單獨創建自己的那個 Bean 實例。
舉例:
- 你有兩個 Web 應用,每個運行一個 Spring 容器,它們各自有自己的單例 Bean 實例。
- 或者你啟動了多個 Spring 容器做測試、隔離等,也會有多個實例。
6.3. 如果使用多個容器或類加載器,則可能出現多實例
類加載器(ClassLoader)不同,雖然類名相同,但被 JVM 認為是不同的類。因此,如果你在不同的類加載器中加載同一個類,也會導致出現“多個單例實例”,不是同一個對象。
典型場景:
- Java EE 容器中不同的部署單元(war包、ear包)
- 插件式架構、模塊化系統
- 熱部署(熱更新)時重載類
6.4. 單例Bean設計時應注意線程安全,避免可變狀態帶來的并發問題
Spring 單例Bean是被多個線程共享的(特別是 Web 應用中,多個請求同時訪問)。如果單例 Bean 內部有可變的成員變量,就會有線程安全風險,可能導致數據錯亂或異常。
設計原則:
- 無狀態設計: Bean 不保存業務狀態,所有狀態通過方法參數傳遞。
- 線程安全的數據結構: 比如使用
ConcurrentHashMap
或AtomicInteger
。 - 同步控制: 必要時用鎖、synchronized 保證并發安全。
6.5. 業務中有狀態的 Bean 一般不要用單例,使用 prototype 或其他作用域
有狀態 Bean:保存用戶會話、操作數據等狀態的 Bean。用單例的話,狀態被多個線程共享,會導致狀態混亂和并發問題。這時應使用 Spring 的其他作用域,比如:
prototype
:每次請求都會創建新實例,避免共享狀態。request
(Web作用域):每個 HTTP 請求一個實例。session
:每個用戶會話一個實例。
博文參考
- 5. 單例模式 — Graphic Design Patterns
- 單例設計模式
- 創建型 - 單例模式(Singleton pattern) | Java 全棧知識體系