歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> 從JVM的角度來看單例模式

從JVM的角度來看單例模式

日期:2017/3/1 9:05:50   编辑:Linux編程

最近在看jvm,發現隨著自己對jvm底層的了解,現在對java代碼可以說是有了全新的認識。今天就從jvm的角度來看一看以前自以為很了解的單例模式。

了解單例模式的人都知道,單例模式有兩種:“餓漢模式”和“懶漢模式”。

引用一段網上對這兩種模式的介紹:

“餓漢模式的特點是加載類時比較慢,但運行時獲取對象的速度比較快,線程安全。餓漢式是線程安全的,在類創建的同時就已經創建好一個靜態的對象供系統使用,以後不在改變。懶漢模式的特點是加載類時比較快,但是在運行時獲取對象的速度比較慢,線程不安全, 懶漢式如果在創建實例對象時不加上synchronized則會導致對象的訪問不是線程安全的。所以在此推薦大家使用餓漢模式。”

筆者先給出結論“上面這段描述可以說是完全不正確,最後給出的結論還算勉強正確,為什麼說勉強正確,因為我不會推薦大家使用餓漢模式,我會直接說就用餓漢模式,懶漢模式在任何情況下都不需要”。

網上這段文字的錯誤主要有兩點
1.懶漢模式線程不安全,如果想線程安全必須加synchronized
2.餓漢模式在加載類時會慢

先來看一下懶漢模式,不用synchronized也能實現線程安全

先來回顧一下懶漢模式的“發展史”

懶漢模式V1.0:

package common;

public class Singleton {
private static Singleton singleton;

public static Singleton getInstance(){
if (singleton==null) {
singleton=new Singleton();
}
return singleton;
}
}

懶漢模式V1.0看起來就很不安全,當同時有兩個線程調用 getInstance()方法時,很容易讓兩個線程都進入if塊導致new 了兩次對象。

於是在某一次大會上,有磚家發布了下面這種叫做DCL(double check lock)的錯誤寫法,因為是磚家發布的,因此這種錯誤寫法在網上廣為流傳,我在公司也看到有人這麼寫,這種我們可以稱為懶漢模式V2.0

package common;

public class Singleton {
private static Singleton singleton;

public static Singleton getInstance(){
if (singleton==null) {
synchronized (Singleton.class) {
if (singleton==null) {
singleton=new Singleton();
}
}
}
return singleton;
}
}

懶漢模式V2.0解決了1.0中可能會new兩次對象的問題,但是依然有問題。

這裡我們先引入一個概念——指令重排序:了優化程序性能而采取的對指令進行重新排序執行的一種手段。

比如:

int a=1;

int b=a+1;

int c=2;

在執行這三句代碼的時候,cpu可以先執行int c=2,再執行另外兩句,這就是指令重排序。

但是很顯然,指令重排序並不是可以隨便亂排的,比如int b=a+1這句依賴了a的值,因此必須要在int a=1之後執行才能保證最終b的值是正確的。因此,指令重排序後,要保證在單個線程裡,執行結果和重排序前是等效的。

這裡為什麼強調是單個線程呢?比如剛剛的例子,假如abc都是全局變量,我們把c=2這一句重排序到第一句,從執行這三句代碼的線程的角度,執行完三句代碼後abc的值和重排序之前是一致的。

但是假設現在有另外一個線程在不停的打印abc的值,那麼因為重排序的關系,在打印結果裡就會出現c=2而ab還沒有被賦值的結果。因此,在指令重排序後,從重排序的這個線程自身來看,重排序後的代碼可以看作是有序的(因為保證運行結果不變),而從其他線程的角度來看,重排序後的代碼是亂序執行的。

回到我們的懶漢模式V2.0,我們現在知道了,當多線程並發的時候,假如第一個線程成功獲取鎖並進入if塊執行singleton=new Singleton(),

這句代碼我們可以看成三步操作:
1.在堆內存中劃分一個Singleton對象實體的空間
2.初始化堆內存中對象實例的數據(字段等)
3.將singleton變量通過指針指向生成的對象實體

這個時候因為指令重排序,可能在步驟2還沒有執行完的時候,步驟3已經執行完了,

這時候singleton變量已經不為null,此時如果有並發的線程執行getInstance()方法,將獲取到一個沒有初始化完成的Singleton對象從而引發錯誤。

為了解決這個問題,我們給singleton變量添加關鍵字volatile得到懶漢模式V3.0:

package common;

public class Singleton {
private static volatile Singleton singleton;

public static Singleton getInstance(){
if (singleton==null) {
synchronized (Singleton.class) {
if (singleton==null) {
singleton=new Singleton();
}
}
}
return singleton;
}
}

這裡用volatile修飾singleton並不是用了volatile的可見性,而是用了java內存模型的“先行發生”(happens-before)原則的其中一條:

Volatile變量規則:對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,這裡的“後面”指時間上的先後順序。

這樣一來就能禁止指令重排序,確保singleton對象是在初始化完成後才能被讀到。

懶漢模式V3.0可以說是懶漢模式的終極形式,經過2次修改終於線程安全了,然而並沒有什麼卵用,因為餓漢模式先天就沒有線程安全問題,而且也並不像網上說的那樣,上來就要創建實例。

餓漢模式解析:

網上一般的說法是,餓漢模式會導致程序啟動慢,因為一上來就要創建實例。相信這麼說的人一定是不了解java的類加載機制。先上個餓漢模式的代碼:

package common;

public class Singleton {
private static final Singleton singleton=new Singleton();

public static Singleton getInstance(){
return singleton;
}
}

可以看到new實例是直接寫在了靜態變量後面,還有一種寫法:

package common;

public class Singleton {
private static final Singleton singleton;

static{
singleton=new Singleton();
}

public static Singleton getInstance(){
return singleton;
}
}

這兩種寫法在編譯後是完全等效的,

類的加載分為5個步驟:加載、驗證、准備、解析、初始化

初始化就是執行編譯後的<cinit>()方法,而<cinit>()方法就是在編譯時將靜態變量賦值和靜態塊合並到一起生成的。

所以說,“餓漢模式”的創建對象是在類加載的初始化階段進行的,那麼類加載的初始化階段在什麼時候進行呢?jvm規范規定有且只有以下7種情況下會進行類加載的初始化階段:
1.使用new關鍵字實例化對象的時候
2.設置或讀取一個類的靜態字段(被final修飾,已在編譯器把結果放入常量池的靜態字段除外)的時候
3.調用一個類的靜態方法的時候
4.使用java.lang.reflect包的方法對類進行反射調用的時候
5.初始化一個類的子類(會首先初始化父類)
6.當虛擬機啟動的時候,初始化包含main方法的主類
7.當使用jdk1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

基本來說就是只有當你以某種方式調用了這個類的時候,它才會進行初始化,而不是說jvm啟動的時候就初始化,所以說假如你的單例類裡只有一個getInstance()方法,那基本上就是當你從其他類調用getInstance()方法的時候才會進行初始化,這事實上和“懶漢模式”是一樣的效果。

當然,也有一種可能就是單例類裡除了getInstance()方法還有一些其他靜態方法,這樣當調用其他靜態方法的時候,也會初始化實例,但是這個很容易解決,只要加個內部類就行了(這種模式叫holder pattern):

package common;

public class Singleton {
private static class SingletonHolder{
private static Singleton instance=new Singleton();
}

public static Singleton getInstance(){
return SingletonHolder.instance;
}
}

這樣只有當調用getInstance()方法的時候,才會初始化內部類SingletonHolder。

總結

經過以上分析,“懶漢模式”實現復雜而且沒有任何獨占優點,“餓漢模式”完勝。

Copyright © Linux教程網 All Rights Reserved