寫在前面
單例對象(Singleton)是一種常用的設計模式。在Java應用中,單例對象能保證在一個JVM中,該對象只有一個實例存在。這樣的模式有幾個好處:
1、某些類創建比較頻繁,對於一些大型的對象,這是一筆很大的系統開銷。
2、省去了new操作符,降低了系統內存的使用頻率,減輕GC壓力。
3、有些類如交易所的核心交易引擎,控制着交易流程,如果該類可以創建多個的話,系統完全亂了。(比如一個軍隊出現了多個司令員同時指揮,肯定會亂成一團),所以只有使用單例模式,才能保證核心交易服務器獨立控制整個流程。
ACTION:
先來一個簡單的單利模式,(返回一個string對象)瞭解一下。
StrUtils.javva
public class StrUtils {
private static String str = null;
private StrUtils(){}
//整個對象的 創建 加鎖 影響性能
public static String getInstance(){
if(str == null){
System.out.println("getInstance");
str = new String(UUID.randomUUID().toString());
}
return str;
}
}
在單線程模式下,幾乎沒問題,放心使用。但是,在多線程環境下就會出現問題,返回的str並不是同一個對象。接下來測試一番。
測試類如下:
Testtt.java
public class Testtt {
public static void main(String[] args) {
for(int i=0; i<20; i++ ){
new Thread(()->System.out.println(StrUtils.getInstance().hashCode())).start();
}
}
}
結果如下:
可以看出 返回的確是不是同一個String對象,而且Instance也執行了多次。 這是因爲 多線程同時到達 if(str == null)這裏面。
所以在多線程給出如下方案s:
方案一:
在 getinstance方法上面添加 synchronized 關鍵字。如下:
public static synchronized String getInstance(){
if(str == null){
System.out.println("getInstance");
str = new String(UUID.randomUUID().toString());
}
return str;
}
可以是可以,但是性能堪憂啊。這樣豈不是每次進來都會堵塞,一個優秀的程序員是不允許這種情況的。
方案二:
由於只是初始化一次,所以只在 創建對象的時候進行阻塞即可。
public static String getInstance(){
if(str == null){
// 只在第一次創建的時候加鎖,往後不會走這個,JVM離間就一個StrUtils,即str
synchronized (StrUtils.class){
System.out.println("getInstance");
str = new String(UUID.randomUUID().toString());
}
}
return str;
}
似乎解決了之前提到的問題,將synchronized關鍵字加在了內部,也就是說當調用的時候是不需要加鎖的,只有在instance爲null,並創建對象的時候才需要加鎖,性能有一定的提升。但是,這樣的情況,還是有可能有問題的,看下面的情況:在Java指令中創建對象和賦值操作是分開進行的,也就是說instance = new Singleton();語句是分兩步執行的。但是JVM並不保證這兩個操作的先後順序,也就是說有可能JVM會爲新的Singleton實例分配空間,然後直接賦值給instance成員,然後再去初始化這個Singleton實例。這樣就可能出錯了,我們以A、B兩個線程爲例:
a>A、B線程同時進入了第一個if判斷
b>A首先進入synchronized塊,由於instance爲null,所以它執行str= new String();
c>由於JVM內部的優化機制,JVM先畫出了一些分配給Singleton實例的空白內存,並賦值給instance成員(注意此時JVM沒有開始初始化這個實例),然後A離開了synchronized塊。
d>B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給調用該方法的程序。
e>此時B線程打算使用Singleton實例,卻發現它沒有被初始化,於是錯誤發生了。
方案三:
private static class StringFactory {
private static String str = new String(UUID.randomUUID().toString());
}
public static String getInstance(){
return StringFactory.str;
}
實際情況是,單例模式使用內部類來維護單例的實現,JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance只被創建一次,並且會保證把賦值給instance的內存初始化完畢,這樣我們就不用擔心上面的問題。同時該方法也只會在第一次調用的時候使用互斥機制,這樣就解決了低性能問題。