關于java的單例設計模式,是項目當中很常用的設計模式。當某個資源,或者某個實例 ,整個項目只使用一份的情況下,我們就需要用這個去創建一個單例,例如工具類等,都是要用到這種模式的。項目中對于這些工具類,往往只需要維持一個對象,然后一直用這個對象就可以了,以此減少內存的開支。
class SingleTonClass {
//餓漢式單例模式 提前創建對象
private static SingleTonClass singleTonClass = new SingleTonClass();
private SingleTonClass() {
}
public static SingleTonClass getInstance() {
return singleTonClass;
}
}
class SimpleClass {
//懶漢式單例模式 等到需求時再去判斷是否需要創建對象
private static SimpleClass simpleClass;
private SimpleClass() {
}
public static SimpleClass getInstance() {
if (simpleClass == null) {//懶漢式
simpleClass = new SimpleClass();
}
return simpleClass;
}
}
上述寫法,如果在線程并發執行的狀態下,很可能會出現多個實例被創建。
所以我們可以改造getInstance()方法,去適應多線程情況。
public static SimpleClass getInstance() {
if (simpleClass == null) {
synchronized (SimpleClass.class) {//減小同步區域 更好的優化性能
if (simpleClass == null) {
simpleClass = new SimpleClass();
}
}
}
return simpleClass;
}
雙重鎖定使得這個函數更加的安全,但是依然有問題
因為simpleClass = new SimpleClass();這段代碼 ,并不是原子性操作,他包含好幾個步驟:
- 為對象分配內存
- 初始化實例對象
- 把引用instance指向分配的內存空間
這個三個步驟并不能保證按序執行,處理器會進行指令重排序優化,存在這樣的情況:
優化重排后執行順序為:1,3,2, 這樣在線程1執行到3時,instance已經不為null了,線程2此時判斷instance!=null,則直接返回instance引用,但現在實例對象還沒有初始化完畢,此時線程2使用instance可能會造成程序崩潰。
現在要解決的問題就是怎樣限制處理器進行指令優化重排。
答案就是用volatile關鍵字 ,他提供了線程之間修改可見性。
class SimpleClass {
private static volatile SimpleClass simpleClass;
private SimpleClass() {
}
public static SimpleClass getInstance() {
if (simpleClass == null) {
synchronized (SimpleClass.class) {
if (simpleClass == null) {
simpleClass = new SimpleClass();
}
}
}
return simpleClass;
}
}
解釋一下volatile關鍵字:
1.保證可見性
可以保證在多線程環境下,變量的修改可見性。每個線程都會在工作內存(類似于寄存器和高速緩存),實例對象都存放在主內存中,在每個線程要使用的時候把主內存中的內容拷貝到線程的工作內存中。使用volatile關鍵字修飾后的變量,保證每次修改了變量需要立即寫回主內存中,同時通知所有的該對變量的緩存失效,保證緩存一致性,其他線程需要使用該共享變量時就要重新從住內存中獲取最新的內容拷貝到工作內存中供處理器使用。這樣就可以保證變量修改的可見性了。但volatile不能保證原子性,比如++操作。
2.提供內存屏障
volatile關鍵字能夠通過提供內存屏障,來保證某些指令順序處理器不能夠優化重排,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
下面是保守策略插入內存屏障:
在每個volatile寫操作的前面插入一個StoreStore屏障。
在每個volatile寫操作的后面插入一個StoreLoad屏障。
在每個volatile讀操作的前面插入一個LoadLoad屏障。
在每個volatile讀操作的后面插入一個LoadLoad屏障。
這樣可以保證在volatile關鍵字修飾的變量的賦值和讀取操作前后兩邊的大的順序不會改變,
在內存屏障前面的順序可以交換,屏障后面的也可以換序,但是不能跨越內存屏障重排執行順序。
好了,現在來看上面的單例模式,這樣就可以保證3步驟(instance賦值操作)是保持最后一步完成,
這樣就不會出現instance在對象沒有初始化時就不為null的情況了。這樣也就實現了正確的單例模式了。
還有一種枚舉,也是單例模式的實現:
public enum SingleEnum{
INSTANCE; //不建議用
}
關于單例模式在android方面的應用:
單例模式一般運用在一些工具類上面。還有,界面之間傳輸數據,有可能會因為數據量太大,造成TransactionTooLargeException異常的,所以這個時候,就需要通過一個幫助類去存儲這段數據,為了保證數據的正確性,這個時候必定就是要用到單例模式的。