單例設計模式概念
單例模式可以說是大多數開發人員在實際中使用最多的,常見的Spring預設建立的bean就是單例模式的。
單例模式有很多好處,比如可節約系統記憶體空間,控制資源的使用。
其中單例模式最重要的是確定對象隻有一個。
簡單來說,保證一個類在記憶體中的對象就一個。
RunTime就是典型的單例設計
我們通過對RunTime類的分析,一窺究竟。
源碼剖析
RunTime.java
目的
控制外界建立對象的個數隻能建立1個對象
開發步驟:
1、 私有化構造方法
2、 在類的内部建立好對象
3、 對外界提供一個公共的get(),傳回一個已經準備好的對象
八種單例模式設計
第1種: 餓漢式
- 類加載到記憶體後,就執行個體化一個單例,JVM保證線程安全
- 簡單實用,推薦使用!
- 唯一缺點:不管用到與否,類裝載時就完成執行個體化
代碼如下:
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};
public static Mgr01 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
優點
- 由于使用了static關鍵字,保證了在引用這個變量時,關于這個變量的是以寫入操作都完成,是以保證了JVM層面的線程安全
缺點
- 不能實作懶加載,造成空間浪費,如果一個類比較大,我們在初始化的時就加載了這個類,但是我們長時間沒有使用這個類,這就導緻了記憶體空間的浪費。
第2種: 靜态代碼塊
/**
* 跟01是一個意思
*/
public class Mgr02 {
private static final Mgr02 INSTANCE;
static {
INSTANCE = new Mgr02();
}
private Mgr02() {};
public static Mgr02 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr02 m1 = Mgr02.getInstance();
Mgr02 m2 = Mgr02.getInstance();
System.out.println(m1 == m2);
}
}
第3種: 懶漢式(線程不安全)
懶漢模式是一種偷懶的模式,在程式初始化時不會建立執行個體,隻有在使用執行個體的時候才會建立執行個體,
- 是以懶漢模式解決了餓漢模式帶來的空間浪費問題
- 同時也引入了其他的問題,我們先來看看下面這個懶漢模式
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03() {
}
public static Mgr03 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Mgr03();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->
System.out.println(Mgr03.getInstance().hashCode())
).start();
}
}
}
優點
- 實作了懶加載,節約了記憶體空間
缺點
- 在不加鎖的情況下,線程不安全,可能出現多份執行個體
- 在加鎖的情況下,會是程式串行化,使系統有嚴重的性能問題
第4種: 懶漢式 (方法上加鎖)
/**
* lazy loading
* 也稱懶漢式
* 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
* 可以通過synchronized解決,但也帶來效率下降
*/
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {
}
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}
第5種: 懶漢式 (同步代碼塊加鎖)
/**
* lazy loading
* 也稱懶漢式
* 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
* 可以通過synchronized解決,但也帶來效率下降
*/
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05() {
}
public static Mgr05 getInstance() {
if (INSTANCE == null) {
//妄圖通過減小同步代碼塊的方式提高效率,然後不可行
synchronized (Mgr05.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr05.getInstance().hashCode());
}).start();
}
}
}
第6種: 懶漢式 (雙重校驗)
/**
* lazy loading
* 也稱懶漢式
* 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
* 可以通過synchronized解決,但也帶來效率下降
*/
public class Mgr06 {
private static volatile Mgr06 INSTANCE; //JIT
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//雙重檢查
synchronized (Mgr06.class) {
if(INSTANCE == null) {
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
- 這裡采用了雙重校驗的方式,對懶漢式單例模式做了線程安全處理。通過加鎖,可以保證同時隻有一個線程走到第二個判空代碼中去,這樣保證了隻建立一個執行個體。
這裡還用到了volatile關鍵字來修飾singleton,其最關鍵的作用是防止指令重排。
第7種: 靜态内部類方式
靜态内部類單例模式也稱單例持有者模式,執行個體由内部類建立,
由于 JVM 在加載外部類的過程中, 是不會加載靜态内部類的, 隻有内部類的屬性/方法被調用時才會被加載,
并初始化其靜态屬性。靜态屬性由static修飾,保證隻被執行個體化一次,并且嚴格保證執行個體化順序。
/**
* 靜态内部類方式
* JVM保證單例
* 加載外部類時不會加載内部類,這樣可以實作懶加載
*/
public class Mgr07 {
private Mgr07() {
}
private static class Mgr07Holder {
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr07.getInstance().hashCode());
}).start();
}
}
}
- 似乎靜态内部類看起來已經是最完美的方法了,其實不是,可能還存在反射攻擊或者反序列化攻擊
第8種: 枚舉實作
- 枚舉類實作單例模式是 effective java 作者極力推薦的單例實作模式,因為枚舉類型是線程安全的,并且隻會裝載一次,設計者充分的利用了枚舉的這個特性來實作單例模式,
- 枚舉的寫法非常簡單,而且枚舉類型是所用單例實作中唯一一種不會被破壞的單例實作模式。
不僅可以解決線程同步,還可以防止反序列化。
/**
* 不僅可以解決線程同步,還可以防止反序列化。
*/
public enum Mgr08 {
INSTANCE;
public void m() {}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr08.INSTANCE.hashCode());
}).start();
}
}
}
單例模式使用場景
單例模式隻允許建立一個對象,是以節省記憶體,加快對象通路速度,是以對象需要被公用的場合适合使用,如多個子產品使用同一個資料源連接配接對象等等。
- 需要頻繁執行個體化然後銷毀的對象。
- 建立對象時耗時過多或者耗資源過多,但又經常用到的對象。
- 有狀态的工具類對象。
- 頻繁通路資料庫或檔案的對象。
應用場景舉例:
1.外部資源:每台計算機有若幹個列印機,但隻能有一個PrinterSpooler,以避免兩個列印作業同時輸出到列印機。内部資源:大多數軟體都有一個(或多個)屬性檔案存放系統配置,這樣的系統應該有一個對象管理這些屬性檔案
2. Windows的Task Manager(任務管理器)就是很典型的單例模式(這個很熟悉吧),想想看,是不是呢,你能打開兩個windows task manager嗎? 不信你自己試試看哦~
3. windows的Recycle Bin(資源回收筒)也是典型的單例應用。在整個系統運作過程中,資源回收筒一直維護着僅有的一個執行個體。
4. 網站的計數器,一般也是采用單例模式實作,否則難以同步。
5. 應用程式的日志應用,一般都何用單例模式實作,這一般是由于共享的日志檔案一直處于打開狀态,因為隻能有一個執行個體去操作,否則内容不好追加。
6. Web應用的配置對象的讀取,一般也應用單例模式,這個是由于配置檔案是共享的資源。
7. 資料庫連接配接池的設計一般也是采用單例模式,因為資料庫連接配接是一種資料庫資源。資料庫軟體系統中使用資料庫連接配接池,主要是節省打開或者關閉資料庫連接配接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,因為何用單例模式來維護,就可以大大降低這種損耗。
8. 多線程的線程池的設計一般也是采用單例模式,這是由于線程池要友善對池中的線程進行控制。
9. 作業系統的檔案系統,也是大的單例模式實作的具體例子,一個作業系統隻能有一個檔案系統。
10. HttpApplication 也是機關例的典型應用。熟悉ASP.Net(IIS)的整個請求生命周期的人應該知道HttpApplication也是單例模式,所有的HttpModule都共享一個HttpApplication執行個體.
網站統計單元
背景
- 在企業網站背景系統中,一般會将網站統計單元進行獨立設計,比如登入人數的統計、IP數量的計數等。在這類需要完成全局統計的過程中,就會用到單例模式,即整個系統隻需要擁有一個計數的全局對象。
- 在網站登入這個高并發場景下,由這個全局對象負責統計目前網站的登入人數、IP等,即節約了網站伺服器的資源,又能保證計數的準确性。
單例模式優缺點
優點:
-
在單例模式中,活動的單例隻有一個執行個體,對單例類的所有執行個體化得到的都是相同的一個執行個體。這樣就
防止其它對象對自己的執行個體化,確定所有的對象都通路一個執行個體
- 單例模式具有一定的伸縮性,類自己來控制執行個體化程序,類就在改變執行個體化程序上有相應的伸縮性。
- 提供了對唯一執行個體的受控通路。
- 由于在系統記憶體中隻存在一個對象,是以可以 節約系統資源,當 需要頻繁建立和銷毀的對象時單例模式無疑可以提高系統的性能。
- 允許可變數目的執行個體。
- 避免對共享資源的多重占用。
缺點:
- 不适用于變化的對象,如果同一類型的對象總是要在不同的用例場景發生變化,單例就會引起資料的錯誤,不能儲存彼此的狀态。
- 由于單利模式中沒有抽象層,是以單例類的擴充有很大的困難。
- 單例類的職責過重,在一定程度上違背了“單一職責原則”。
- 濫用單例将帶來一些負面問題,如為了節省資源将資料庫連接配接池對象設計為的單例類,可能會導緻共享連接配接池對象的程式過多而出現連接配接池溢出;如果執行個體化的對象長時間不被利用,系統會認為是垃圾而被回收,這将導緻對象狀态的丢失。
使用注意事項:
- 使用時不能用反射模式建立單例,否則會執行個體化一個新的對象
- 使用懶單例模式時注意線程安全問題
- 餓單例模式和懶單例模式構造方法都是私有的,因而是不能被繼承的,有些單例模式可以被繼承(如登記式模式)