在很多設計模式的書籍中,我們都可以看到類似下面的單例模式的實現代碼,一般稱爲Double-checked locking(DCL)
01 |
public class
Singleton { |
03 |
private
static Singleton instance; |
09 |
public
static Singleton getInstance() { |
10 |
if
(instance == null ) { |
11 |
synchronized
(Singleton. class ) { |
12 |
if
(instance == null ) { |
13 |
instance =
new Singleton(); |
這樣子的代碼看起來很完美,可以解決instance的延遲初始化。只是,事實往往不是如此。
問題在於instance = new Singleton();這行代碼。
在我們看來,這行代碼的意義大概是下面這樣子的
mem = allocate(); //收集內存
ctorSingleton(mem); //調用構造函數
instance = mem; //把地址傳給instance
這行代碼在Java虛擬機(JVM)看來,卻可能是下面的三個步驟(亂序執行的機制):
mem = allocate(); //收集內存
instance = mem; //把地址傳給instance
ctorSingleton(instance); //調用構造函數
下面我們來假設一個場景。
- 線程A調用getInstance函數並且執行到//4。但是線程A只執行到賦值語句,還沒有調用構造函數。此時,instance已經不是null了,但是對象還沒有初始化。
- 很不幸線程A這時正好被掛起。
- 線程B獲得執行的權力,然後也開始調用getInstance。線程B在//1發現instance已經不是null了,於是就返回對象了,但是這個對象還沒有初始化,於是對這個對象進行操作就出錯了。
問題就出在instance被提前初始化了。
解決方案一,不使用延遲加載:
01 |
public class
Singleton { |
03 |
private
static Singleton instance =
new Singleton(); |
09 |
public
static Singleton getInstance() { |
JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance只被創建一次,並且會保證把賦值給instance的內存初始化完畢。
解決方案二,利用一個內部類來實現延遲加載:
01 |
public class
Singleton { |
07 |
private
static class
SingletonContainer { |
08 |
private
static Singleton instance =
new Singleton(); |
11 |
public
static Singleton getInstance() { |
12 |
return
SingletonContainer.instance; |
這兩種方案都是利用了JVM的類加載機制的互斥。
方案二的延遲加載實現是因爲,只有在第一次調用Singleton.getInstance()函數時,JVM纔會去加載SingletonContainer,並且初始化instance。
不只Java存在這個問題,C/C++由於CPU的亂序執行機制,也同樣存在這樣的問題。