原文出處: 張新強html
單例(Singleton)
應該是開發者們最熟悉的設計模式了,而且好像也是最容易實現的——基本上每一個開發者都可以隨手寫出——可是,真的是這樣嗎?
做爲一個Java開發者,也許你以爲本身對單例模式的瞭解已經足夠多了。我並不想危言聳據說必定還有你不知道的——畢竟我本身的瞭解也的確有限,但究竟你本身瞭解的程度到底怎樣呢?往下看,咱們一塊兒來聊聊看~java
單例對象的類必須保證只有一個實例存在
——這是維基百科上對單例的定義,這也能夠做爲對意圖實現單例模式的代碼進行檢驗的標準。git
對單例的實現能夠分爲兩大類——懶漢式
和餓漢式
,他們的區別在於:懶漢式
:指全局的單例實例在第一次被使用時構建。餓漢式
:指全局的單例實例在類裝載時構建。github
從它們的區別也能看出來,平常咱們使用的較多的應該是懶漢式
的單例,畢竟按需加載才能作到資源的最大化利用嘛~shell
先來看一下懶漢式單例的實現方式。編程
看最簡單的寫法Version 1:設計模式
1
2
3
4
5
6
7
8
9
10
|
// Version 1
public
class
Single1 {
private
static
Single1 instance;
public
static
Single1 getInstance() {
if
(instance ==
null
) {
instance =
new
Single1();
}
return
instance;
}
}
|
或者再進一步,把構造器改成私有的,這樣可以防止被外部的類調用。安全
1
2
3
4
5
6
7
8
9
10
11
|
// Version 1.1
public
class
Single1 {
private
static
Single1 instance;
private
Single1() {}
public
static
Single1 getInstance() {
if
(instance ==
null
) {
instance =
new
Single1();
}
return
instance;
}
}
|
我彷彿記得當初學校的教科書就是這麼教的?—— 每次獲取instance以前先進行判斷,若是instance爲空就new一個出來,不然就直接返回已存在的instance。
這種寫法在大多數的時候也是沒問題的。問題在於,當多線程工做的時候,若是有多個線程同時運行到if (instance == null)
,都判斷爲null,那麼兩個線程就各自會建立一個實例——這樣一來,就不是單例了。多線程
那既然可能會由於多線程致使問題,那麼加上一個同步鎖吧!
修改後的代碼以下,相對於Version1.1,只是在方法簽名上多加了一個synchronized
:併發
1
2
3
4
5
6
7
8
9
10
11
|
// Version 2
public
class
Single2 {
private
static
Single2 instance;
private
Single2() {}
public
static
synchronized
Single2 getInstance() {
if
(instance ==
null
) {
instance =
new
Single2();
}
return
instance;
}
}
|
OK,加上synchronized
關鍵字以後,getInstance方法就會鎖上了。若是有兩個線程(T一、T2)同時執行到這個方法時,會有其中一個線程T1得到同步鎖,得以繼續執行,而另外一個線程T2則須要等待,當第T1執行完畢getInstance以後(完成了null判斷、對象建立、得到返回值以後),T2線程纔會執行執行。——因此這端代碼也就避免了Version1中,可能出現由於多線程致使多個實例的狀況。
可是,這種寫法也有一個問題:給gitInstance方法加鎖,雖然會避免了可能會出現的多個實例問題,可是會強制除T1以外的全部線程等待,實際上會對程序的執行效率形成負面影響。
Version2代碼相對於Version1d代碼的效率問題,實際上是爲了解決1%概率的問題,而使用了一個100%出現的防禦盾。那有一個優化的思路,就是把100%出現的防禦盾,也改成1%的概率出現,使之只出如今可能會致使多個實例出現的地方。
——有沒有這樣的方法呢?固然是有的,改進後的代碼Vsersion3以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// Version 3
public
class
Single3 {
private
static
Single3 instance;
private
Single3() {}
public
static
Single3 getInstance() {
if
(instance ==
null
) {
synchronized
(Single3.
class
) {
if
(instance ==
null
) {
instance =
new
Single3();
}
}
}
return
instance;
}
}
|
這個版本的代碼看起來有點複雜,注意其中有兩次if (instance == null)
的判斷,這個叫作『雙重檢查 Double-Check』。
if (instance == null)
,實際上是爲了解決Version2中的效率問題,只有instance爲null的時候,才進入synchronized
的代碼段——大大減小了概率。if (instance == null)
,則是跟Version2同樣,是爲了防止可能出現多個實例的狀況。—— 這段代碼看起來已經完美無瑕了。
……
……
……
—— 固然,只是『看起來』,仍是有小几率出現問題的。
這弄清楚爲何這裏可能出現問題,首先,咱們須要弄清楚幾個概念:原子操做
、指令重排
。
簡單來講,原子操做(atomic)
就是不可分割的操做,在計算機中,就是指不會由於線程調度被打斷的操做。
好比,簡單的賦值是一個原子操做:
m = 6; // 這是個原子操做
假如m原先的值爲0,那麼對於這個操做,要麼執行成功m變成了6,要麼是沒執行m仍是0,而不會出現諸如m=3這種中間態——即便是在併發的線程中。
而,聲明並賦值就不是一個原子操做:
int n = 6; // 這不是一個原子操做
對於這個語句,至少有兩個操做:
①聲明一個變量n
②給n賦值爲6
——這樣就會有一箇中間狀態:變量n已經被聲明瞭可是尚未被賦值的狀態。
——這樣,在多線程中,因爲線程執行順序的不肯定性,若是兩個線程都使用m,就可能會致使不穩定的結果出現。
簡單來講,就是計算機爲了提升執行效率,會作的一些優化,在不影響最終結果的狀況下,可能會對一些語句的執行順序進行調整。
好比,這一段代碼:
1
2
3
4
|
int
a ;
// 語句1
a =
8
;
// 語句2
int
b =
9
;
// 語句3
int
c = a + b ;
// 語句4
|
正常來講,對於順序結構,執行的順序是自上到下,也即1234。
可是,因爲指令重排
的緣由,由於不影響最終的結果,因此,實際執行的順序可能會變成3124或者1324。
因爲語句3和4沒有原子性的問題,語句3和語句4也可能會拆分紅原子操做,再重排。
——也就是說,對於非原子性的操做,在不影響最終結果的狀況下,其拆分紅的原子操做可能會被從新排列執行順序。
OK,瞭解了原子操做
和指令重排
的概念以後,咱們再繼續看Version3代碼的問題。
下面這段話直接從陳皓的文章(深刻淺出單實例SINGLETON設計模式)中複製而來:
主要在於singleton = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。
1. 給 singleton 分配內存
2. 調用 Singleton 的構造函數來初始化成員變量,造成實例
3. 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null 了)
可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。
再稍微解釋一下,就是說,因爲有一個『instance已經不爲null可是仍沒有完成初始化』的中間狀態,而這個時候,若是有其餘線程恰好運行到第一層if (instance == null)
這裏,這裏讀取到的instance已經不爲null了,因此就直接把這個中間狀態的instance拿去用了,就會產生問題。
這裏的關鍵在於——線程T1對instance的寫操做沒有完成,線程T2就執行了讀操做。
對於Version3中可能出現的問題(固然這種機率已經很是小了,但畢竟仍是有的嘛~),解決方案是:只須要給instance的聲明加上volatile
關鍵字便可,Version4版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// Version 4
public
class
Single4 {
private
static
volatile
Single4 instance;
private
Single4() {}
public
static
Single4 getInstance() {
if
(instance ==
null
) {
synchronized
(Single4.
class
) {
if
(instance ==
null
) {
instance =
new
Single4();
}
}
}
return
instance;
}
}
|
volatile
關鍵字的一個做用是禁止指令重排
,把instance聲明爲volatile
以後,對它的寫操做就會有一個內存屏障
(什麼是內存屏障?),這樣,在它的賦值完成以前,就不用會調用讀操做。
注意:volatile阻止的不singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操做([1-2-3])完成以前,不會調用讀操做(
if (instance == null)
)。
——也就完全防止了Version3中的問題發生。
——好了,如今完全沒什麼問題了吧?
……
……
……
好了,別緊張,的確沒問題了。大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()
就是用這種方法來實現的。
……
……
……
不過,非要挑點刺的話仍是能挑出來的,就是這個寫法有些複雜了,不夠優雅、簡潔。
(傲嬌臉)(  ̄ー ̄)
下面再聊瞭解一下餓漢式的單例。
如上所說,餓漢式
單例是指:指全局的單例實例在類裝載時構建的實現方式。
因爲類裝載的過程是由類加載器(ClassLoader)來執行的,這個過程也是由JVM來保證同步的,因此這種方式先天就有一個優點——可以免疫許多由多線程引發的問題。
餓漢式
單例的實現以下:
1
2
3
4
5
6
7
8
|
//餓漢式實現
public
class
SingleB {
private
static
final
SingleB INSTANCE =
new
SingleB();
private
SingleB() {}
public
static
SingleB getInstance() {
return
INSTANCE;
}
}
|
對於一個餓漢式單例的寫法來講,它基本上是完美的了。
因此它的缺點也就只是餓漢式單例自己的缺點所在了——因爲INSTANCE的初始化是在類加載時進行的,而類的加載是由ClassLoader來作的,因此開發者原本對於它初始化的時機就很難去準確把握:
固然,若是所需的單例佔用的資源不多,而且也不依賴於其餘數據,那麼這種實現方式也是很好的。
前面提到了單例在類裝載時被實例化,那究竟何時纔是『類裝載時』呢?
不嚴格的說,大體有這麼幾個條件會觸發一個類被加載:
1. new一個對象時
2. 使用反射建立它的實例時
3. 子類被加載時,若是父類還沒被加載,就先加載父類
4. jvm啓動時執行的主類會首先被加載
《Effective Java》一書的初版中推薦了一箇中寫法:
1
2
3
4
5
6
7
8
9
10
|
// Effective Java 初版推薦寫法
public
class
Singleton {
private
static
class
SingletonHolder {
private
static
final
Singleton INSTANCE =
new
Singleton();
}
private
Singleton (){}
public
static
final
Singleton getInstance() {
return
SingletonHolder.INSTANCE;
}
}
|
這種寫法很是巧妙:
——它利用了ClassLoader來保證了同步,同時又能讓開發者控制類加載的時機。從內部看是一個餓漢式的單例,可是從外部看來,又的確是懶漢式的實現。
簡直是神乎其技。
你覺得到這就算完了?不,並無,由於厲害的大神又發現了其餘的方法。
《Effective Java》的做者在這本書的第二版又推薦了另一種方法,來直接看代碼:
1
2
3
4
5
6
7
8
9
10
|
// Effective Java 第二版推薦寫法
public
enum
SingleInstance {
INSTANCE;
public
void
fun1() {
// do something
}
}
// 使用
SingleInstance.INSTANCE.fun1();
|
看到了麼?這是一個枚舉類型……連class都不用了,極簡。
因爲建立枚舉實例的過程是線程安全的,因此這種寫法也沒有同步的問題。
做者對這個方法的評價:
這種寫法在功能上與共有域方法相近,可是它更簡潔,無償地提供了序列化機制,絕對防止對此實例化,即便是在面對複雜的序列化或者反射攻擊的時候。雖然這中方法尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。
枚舉單例這種方法問世一些,許多分析文章都稱它是實現單例的最完美方法——寫法超級簡單,並且又能解決大部分的問題。
不過我我的認爲這種方法雖然很優秀,可是它仍然不是完美的——好比,在須要繼承的場景,它就不適用了。
OK,看到這裏,你還會以爲單例模式是最簡單的設計模式了麼?再回頭看一下你以前代碼中的單例實現,以爲是無懈可擊的麼?
可能咱們在實際的開發中,對單例的實現並無那麼嚴格的要求。好比,我若是能保證全部的getInstance都是在一個線程的話,那其實第一種最簡單的教科書方式就夠用了。再好比,有時候,個人單例變成了多例也可能對程序沒什麼太大影響……
可是,若是咱們能瞭解更多其中的細節,那麼若是哪天程序出了些問題,咱們起碼能多一個排查問題的點。早點解決問題,就能早點回家吃飯……:-D
—— 還有,完美的方案是不存在,任何方式都會有一個『度』的問題。好比,你的以爲代碼已經無懈可擊了,可是由於你用的是JAVA語言,可能ClassLoader有些BUG啊……你的代碼誰運行在JVM上的,可能JVM自己有BUG啊……你的代碼運行在手機上,可能手機系統有問題啊……你生活在這個宇宙裏,可能宇宙自己有些BUG啊……o(╯□╰)o
因此,盡力作到能作到的最好就好了。
—— 感謝你花費了很多時間看到這裏,希望你沒有以爲虛度。
深刻淺出單實例SINGLETON設計模式:http://coolshell.cn/articles/265.html
Java併發編程:volatile關鍵字解析:http://www.cnblogs.com/dolphin0520/p/3920373.html
爲何volatile不能保證原子性而Atomic能夠?: http://www.cnblogs.com/Mainz/p/3556430.html
類在何時加載和初始化?http://www.importnew.com/6579.html
https://github.com/barryhappy
http://www.barryzhang.com