JAVA設計模式之單例模式

一、概念

java中單例模式是一種常見的設計模式,單例模式的寫法有好幾種,這裏主要介紹三種:懶漢式單例、餓漢式單例。

單例模式有以下特點:

 

  1. 單例模式確保當前系統只有一個實例
  2. 單例類必須自己創建自己的唯一實例
  3. 單例類必須給其他對象提供這一實例

單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若干個打印機,但只能有一個Printer Spooler,以避免兩個打印作業同時輸出到打印機中。每臺計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是爲了避免不一致狀態,避免政出多頭。

 

二、代碼實現

懶漢式單例

//懶漢式單例類.在第一次調用的時候實例化自己 
public class Singleton {
    private Singleton() {}
    private static Singleton single=null;
    //靜態工廠方法 
    public static Singleton getInstance() {
         if (single == null) {  
             single = new Singleton();
         }  
        return single;
    }
}

 

Singleton通過將構造方法限定爲private避免了類在外部被實例化,在同一個虛擬機範圍內,Singleton的唯一實例只能通過getInstance()方法訪問。(事實上,通過Java反射機制是能夠實例化構造方法爲private的類的,那基本上會使所有的Java單例實現失效。)

但是以上懶漢式單例的實現沒有考慮線程安全問題,它是線程不安全的,併發環境下很可能出現多個Singleton實例,要實現線程安全,有以下方式,都是對getInstance這個方法改造,保證了懶漢式單例的線程安全。

 

1、getInstance方法上加同步

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

在方法調用上加了同步,雖然線程安全了,但是每次都要同步,會影響性能,畢竟大多數情況下是不需要同步的。

 

2、雙重檢查鎖定

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

爲什麼需要兩次if判斷呢?

因爲在高併發時,兩個線程AB都同時進入了第一個if判斷,若A搶到了鎖,實例化了一個對象,釋放鎖,然後B在搶到鎖。此時如果不做第二次if判斷的話,B線程將會再實例化一個對象。

在getInstance中做了兩次null檢查,確保了只有第一次調用單例的時候纔會做同步,這樣也是線程安全的,同時避免了每次都同步的性能損耗。

但這種雙重檢查鎖定也是不安全的,不安全在這行代碼上,singleton=new Singleton(),這一行代碼可以分解爲三行僞代碼

memory=allocate();        //1:分配對象的內存空間
ctorInstance(memory)      //2: 初始化對象
instance=memory;          //3:設置instance指向剛分配的內存地址

上面的3行僞代碼中,第二行和第三行可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的)。2和3重排序之後的執行時序如下。

memory=allocate();        //1:分配對象的內存空間
instance=memory;          //2:設置instance指向剛分配的內存地址
ctorInstance(memory)      //3: 初始化對象

產生不安全的例子如下

多線程執行時序表

時間 線程A 線程B
t1 A1:分配對象的內存空間  
t2 A3:設置instance指向內存空間  
t3   B1:判斷instance是否爲空
t4   B2:由於instance不爲null,線程B將訪問instance引用對象
t5 A2:初始化對象  
t6 A4:訪問instance引用對象  

 

3、基於volatile的單例

public class SafeDoubleCheckedLocking{
        private volatile static Instance instance
        
        public static Instance getInstance(){
            synchonized(SafeDoubleCheckedLocking.class){
                if(instance==null){
                    instance=new Instance();//instance爲volatile,這個操作具有原子性。不會導致重排序
                }
                
            }            
        }

}

4、基於類初始化的單例

原理:JVM在類的初始化階段(即在Class被加載後,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。

public class InstanceFactory{
    private static class InstanceHolder{
        public static Instance instance=new Instance();
    }
    
    public static Instance getInstance(){
        return InstanceHolder.instance();//這裏將導致InstanceHolder類被初始化.
    }
}

JVM在類初始化期間會獲取這個初始化鎖,並且每個線程至少獲取一次鎖來確保這個類已經被初始化了。

5、餓漢式單例

//餓漢式單例類.在類初始化時,已經自行實例化   
public class Singleton1 {  
    private Singleton1() {}  
    private static final Singleton1 single = new Singleton1();  
    //靜態工廠方法   
    public static Singleton1 getInstance() {  
        return single;  
    }  
}  

餓漢式在類創建的同時就已經創建好一個靜態的對象供系統使用,以後不再改變,所以天生是線程安全的。

 

 

三、資源加載和性能

餓漢式在類創建的同時就實例化一個靜態對象出來,不管之後會不會使用這個單例,都會佔據一定的內存,但是相應的,在第一次調用時速度也會更快,因爲其資源已經初始化完成。

而懶漢式顧名思義,會延遲加載,在第一次使用該單例的時候纔會實例化對象出來,第一次調用時要做初始化,如果要做的工作比較多,性能上會有些延遲,之後就和餓漢式一樣了。

 

轉載地址:https://blog.csdn.net/jason0539/article/details/23297037/

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章