前言
只有光頭才能變強
回顧前面:
本來打算沒那麼快更新的,這陣子在刷Spring的書籍。在看Spring的時候又經常會看到“單例”,“工廠”這些字樣。
所以,就先來說說單例和工廠設計模式啦,這兩種模式也是很常見的,我看很多面經都會遇到這兩種模式~
本文主要講解單例設計模式,如果有錯的地方希望能多多包涵,並不吝在評論區指正!
一、單例模式概述
單例模式定義很簡單:一個類中能建立一個例項,所以稱之為單例!
那我們什麼時候會用到單例模式呢??
- 那我們想想既然一個類中只能建立一個例項了,那麼可以說這是跟類的狀態與物件無關的了。
- 頻繁建立物件、管理物件是一件耗費資源的事,我們只需要建立一個物件來用就足夠了!
學過Java Web的同學可能就知道:
- Servlet是單例的
- Struts2是多例的
- SpringMVC是單例的
那既然多例是頻繁建立物件、需要管理物件的,那Struts2為什麼要多例呢??
- 主要由於設計層面上的問題,Struts2是基於Filter攔截類的,ognl引擎對變數是注入的。所以它要設計成多例的~
能使用一個物件來做就不用例項化多個物件!這就能減少我們空間和記憶體的開銷~
那有可能有的人又會想了:我們使用靜態類.doSomething()
和使用單例物件呼叫方法的效果是一樣的啊。
- 沒錯,效果就是一樣的。使用
靜態類.doSomething()
體現的是基於物件,而使用單例設計模式體現的是物件導向。
二、編寫單例模式的程式碼
編寫單例模式的程式碼其實很簡單,就分了三步:
- 將建構函式私有化
- 在類的內部建立例項
- 提供獲取唯一例項的方法
2.1餓漢式
根據上面的步驟,我們就可以輕鬆完成建立單例物件了。
public class Java3y {
// 1.將建構函式私有化,不可以通過new的方式來建立物件
private Java3y(){}
// 2.在類的內部建立自行例項
private static final Java3y java3y = new Java3y();
// 3.提供獲取唯一例項的方法
public static Java3y getJava3y() {
return java3y;
}
}
複製程式碼
這種程式碼我們稱之為:“餓漢式”:
- 一上來就建立物件了,如果該例項從始至終都沒被使用過,則會造成記憶體浪費。
2.2簡單懶漢式
既然說一上來就建立物件,如果沒有用過會造成記憶體浪費:
- 那麼我們就設計用到的時候再建立物件!
public class Java3y {
// 1.將建構函式私有化,不可以通過new的方式來建立物件
private Java3y(){}
// 2.1先不建立物件,等用到的時候再建立
private static Java3y java3y = null;
// 2.1呼叫到這個方法了,證明是要被用到的了
public static Java3y getJava3y() {
// 3. 如果這個物件引用為null,我們就建立並返回出去
if (java3y == null) {
java3y = new Java3y();
}
return java3y;
}
}
複製程式碼
上面的程式碼行不行??在單執行緒環境下是行的,在多執行緒環境下就不行了!
- 如果不知道為啥在多執行緒環境下不行的同學可參考我之前的博文:多執行緒基礎必要知識點!看了學習多執行緒事半功倍
要解決也很簡單,我們只要加鎖就行了:
2.3雙重檢測機制(DCL)懶漢式
上面那種直接在方法上加鎖的方式其實不夠好,因為在方法上加了內建鎖在多執行緒環境下效能會比較低下,所以我們可以將鎖的範圍縮小。
public class Java3y {
private Java3y() {
}
private static Java3y java3y = null;
public static Java3y getJava3y() {
if (java3y == null) {
// 將鎖的範圍縮小,提高效能
synchronized (Java3y.class) {
java3y = new Java3y();
}
}
return java3y;
}
}
複製程式碼
那上面的程式碼可行嗎??不行,因為雖然加了鎖,但還是有可能建立出兩個物件出來的:
- 執行緒A和執行緒B同時呼叫
getJava3y()
方法,他們同時判斷java==null
,得出的結果都是為null,所以進入了if程式碼塊了 - 此時執行緒A得到CPU的控制權-->進入同步程式碼塊-->建立物件-->返回物件
- 執行緒A完成了以後,此時執行緒B得到了CPU的控制權。同樣是-->進入同步程式碼塊-->建立物件-->返回物件
- 很明顯的是:Java3y類返回了不止一個例項!所以上面的程式碼是不行的!
有的同學可能覺得我瞎吹比,明明加鎖了還不行?我們來測試一下:
public class TestDemo {
public static void main(String[] args) {
// 執行緒A
new Thread(() -> {
// 建立單例物件
Java3y java3y = Java3y.getJava3y();
System.out.println(java3y);
}).start();
// 執行緒B
new Thread(() -> {
// 建立單例物件
Java3y java3y = Java3y.getJava3y();
System.out.println(java3y);
}).start();
// 執行緒C
new Thread(() -> {
// 建立單例物件
Java3y java3y = Java3y.getJava3y();
System.out.println(java3y);
}).start();
}
}
複製程式碼
可以看到,列印出的物件不單單隻有一個的!
厲害的程式設計師又想到了:進入同步程式碼塊時再判斷一下物件是否存在就穩了吧!
- 所以,有了下面的程式碼
public class Java3y {
private Java3y() {
}
private static Java3y java3y = null;
public static Java3y getJava3y() {
if (java3y == null) {
// 將鎖的範圍縮小,提高效能
synchronized (Java3y.class) {
// 再判斷一次是否為null
if (java3y == null) {
java3y = new Java3y();
}
}
}
return java3y;
}
}
複製程式碼
其實還不穩!這裡會有重排序的問題:
本來想測試重排序問題的效果的,一直沒測試出來~~~有相關測試程式碼的希望可以告訴我怎麼能測出來....
要解決也十分簡單,加上我們的volatile關鍵字就可以了,volatile有記憶體屏障的功能!
具體可參考資料:
- www.zhihu.com/question/35…----雙重檢查鎖失效是因為物件的初始化並非原子操作?
- ifeve.com/doublecheck…---有關“雙重檢查鎖定失效”的說明
- my.oschina.net/u/866190/bl…----正確使用雙重檢查鎖(DCL)
所以說,完整的DCL程式碼是這樣子的:
public class Java3y {
private Java3y() {
}
private static volatile Java3y java3y = null;
public static Java3y getJava3y() {
if (java3y == null) {
// 將鎖的範圍縮小,提高效能
synchronized (Java3y.class) {
// 再判斷一次是否為null
if (java3y == null) {
java3y = new Java3y();
}
}
}
return java3y;
}
}
複製程式碼
再說明:
2.4靜態內部類懶漢式
還可以使用靜態內部類這種巧妙的方式來實現單例模式!它的原理是這樣的:
- 當任何一個執行緒第一次呼叫
getInstance()
時,都會使SingletonHolder被載入和被初始化,此時靜態初始化器將執行Singleton的初始化操作。(被呼叫時才進行初始化!) - 初始化靜態資料時,Java提供了的執行緒安全性保證。(所以不需要任何的同步)
public class Java3y {
private Java3y() {
}
// 使用內部類的方式來實現懶載入
private static class LazyHolder {
// 建立單例物件
private static final Java3y INSTANCE = new Java3y();
}
// 獲取物件
public static final Java3y getInstance() {
return LazyHolder.INSTANCE;
}
}
複製程式碼
靜態內部類這種方式是非常推薦使用的!很多人沒接觸過單例模式的都不知道有這種寫法,這種寫法很優化也高效!
參考資料:
- www.zhihu.com/question/35…----java 單例模式通過內部靜態類的方式?
2.5列舉方式實現
使用列舉就非常簡單了:
public enum Java3y3y {
JAVA_3_Y_3_Y,
}
複製程式碼
那這種有啥好處??列舉的方式實現:
- 簡單,直接寫就行了
- 防止多次例項化,即使是在面對複雜的序列化或者反射攻擊的時候(安全)!
這種也較為推薦使用!
三、總結
總的來說單例模式寫法有5種:
- 餓漢式
- 簡單懶漢式(在方法加鎖)
- DCL雙重檢測加鎖(進階懶漢式)
- 靜態內部類實現懶漢式(最推薦寫法)
- 列舉方式(最安全、簡潔寫法)
明天估計寫的是工廠模式了,敬請期待哦~~~
參考資料:
- 《設計模式之禪》
- www.cnblogs.com/seesea125/a…---為什麼要用單例模式?
- zhuanlan.zhihu.com/p/32310340---聖誕節,讓我們聊聊單例模式
- zhuanlan.zhihu.com/p/34406410---單例模式詳解
- www.nowamagic.net/librarys/ve…---使用單例模式需要注意的幾個問題
- zhuanlan.zhihu.com/p/23713957---Java設計模式(一)-單例模式
如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關注微信公眾號:Java3y。
文章的目錄導航: