Java設計模式之單例模式的七種寫法

什麼是單例模式?


單例模式是一種常見的設計模式,單例模式的寫法有很多種,這裏主要介紹三種: 懶漢式單例模式、餓漢式單例模式、登記式單例 。


單例模式有以下特點:


1、單例類只能有一個實例。

2、單例類必須自己創建自己唯一的實例。

3、單例類必須給所有其它對象提供這一實例。


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


在將單例之前,要做一次基礎知識的科普行動,大家都知道Java類加載器加載內容的順序:

1、從上往下(Java的變量需要聲明才能使用)

2、先靜態後動態(對象實例化) (靜態塊和static關鍵字修飾在實例化以前分配內存空間)

3、先屬性後方法(成員變量不能定義在方法中,只能定義在class下)


懶漢式單例(4種寫法)

懶漢式顧名思義:需要用到的時候纔會初始化

餓漢式:不管用不用先實例化

註冊登記式:相當於有一個容器裝載實例,在實例產生之前會先檢查一下容器看有沒有,如果有就直接取出來使用,如果沒有就先new一個放進去,然後在後面的人使用,Spring IOC就是一種典型的註冊登記單例


第一種寫法:

/**
 * Created by xingyuchao on 2018/1/20.
 * 懶漢式單例類,在第一次使用的時候實例化自己
 */
public class Singleton {

    //1.第一步先將構造方法私有化
    private Singleton(){}

    //2.然後聲明一個靜態變量保存單例的引用
    private static Singleton single = null;

    //3.通過提供一個靜態方法來獲得單例的引用
    public static Singleton getInstance(){
        if(single == null){
            single = new Singleton();
        }
        return single;
    }
}
Singleton1通過將構造方法限定爲private避免了類在外部被實例化,在同一個虛擬機範圍內,Signleton1的唯一實例只能通過getInstance()方法訪問。

事實上,通過Java反射機制是能否實現實例化構造方法爲private的類的,那基本上會使所有的Java單例實現失效,此問題在此處不做討論

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

第二種寫法:在getInstance()方法上加同步
/**
 * Created by xingyuchao on 2018/1/29.
 * 懶漢式單例類,保證線程安全
 */
public class Singleton2 {

    //1.第一步先將構造方法私有化
    private Singleton2(){}

    //2.然後聲明一個靜態變量保存單例的引用
    private static Singleton2 single = null;

    //3.通過提供一個靜態方法來獲得單例的引用
    // 爲了保證線程環境下正確訪問,給方法上加上同步鎖synchronized
    public static synchronized Singleton2 getInstance(){
        if(single == null){
            single = new Singleton2();
        }
        return single;
    }
}
第三種寫法:雙重檢測機制

/**
 * Created by xingyuchao on 2018/1/29.
 * 懶漢式單例類,保證線程安全  雙重檢測機制
 */
public class Singleton3 {

    //1.第一步先將構造方法私有化
    private Singleton3(){}

    //2.然後聲明一個靜態變量保存單例的引用
    private static Singleton3 single = null;

    //3.通過提供一個靜態方法來獲得單例的引用
    // 爲了保證線程環境下的另一種實現方式,雙重鎖檢查
    public static synchronized Singleton3 getInstance(){
        if(single == null){
            single = new Singleton3();
        }
        return single;
    }
}
第四種:靜態內部類
/**
 * Created by xingyuchao on 2018/1/29.
 * 懶漢式單例類,通過靜態內部類實現
 */
public class Singleton4 {

    //1. 先聲明一個靜態內部類
    //內部類的初始化,需要依賴主類
    //也就是說,當JVM加載Singleton4類的時候LazyHolder類也會被加載
    //只是目前還沒有被實例化,需要等主類先實例化後,內部類纔開始實例化
    private static class LazyHolder{
        //final是爲了防止內部類將這個屬性值覆蓋掉
        private static final Singleton4 INSTANCE = new Singleton4();
    }

    //2. 將默認構造方法私有化
    private Singleton4(){}

    //3.同樣提供靜態方法獲取實例

    //當getInstance方法第一次被調用的時候,它第一次讀取LazyHolder.INSTANCE,內部類LazyHolder類得到初始化;
    // 而這個類在裝載並被初始化的時候,會初始化它的靜態域,從而創建Singleton4的實例,由於是靜態的域,因此只會在
    // 虛擬機裝載類的時候初始化一次,並由虛擬機來保證它的線程安全性。這個模式的優勢在於,getInstance方法並沒有被同步,
    // 並且只是執行一個域的訪問,因此延遲初始化並沒有增加任何訪問成本。

    //此處加final是爲了防止子類重寫父類方法
    public static final Singleton4 getInstance(){
        return LazyHolder.INSTANCE;
    }
}

餓漢式單例(1種寫法)

/**
 * Created by xingyuchao on 2018/1/29.
 * 餓漢式單例類,在類初始化時,已經自行初始化,不會產生線程安全問題
 */
public class Singleton5 {

    //1.同樣也是將默認構造方法私有化
    private Singleton5(){}

    //2.聲明靜態變量,在類初始化之前就初始化變量,將對象引用保存
    //相反的如果這個單例對象一直沒使用,那麼內存空間也就被浪費掉了
    private static final Singleton5 singleton = new Singleton5();

    //3.開放靜態方法,獲取實例
    public static Singleton5 getSingleton(){
        return singleton;
    }

}

枚舉式單例(1種寫法)

public class DBConnection {}
/**
 * Created by xingyuchao on 2018/1/29.
 * 枚舉式單例
 */
public enum Singleton6 {


    DATASOURCE;

    private DBConnection connection = null;

    private Singleton6() {
        connection = new DBConnection();
    }

    public DBConnection getConnection() {
        return connection;
    }
}

/**
 * Created by xingyuchao on 2018/1/29.
 */
public class Main {

    public static void main(String[] args) {
        DBConnection dbConnection1 = Singleton6.DATASOURCE.getConnection();

        DBConnection dbConnection2 = Singleton6.DATASOURCE.getConnection();

        System.out.println(dbConnection1 == dbConnection2); //true true  結果表明兩次獲取返回了相同的實例。
    }
}
這種方式不僅能避免多線程同步問題,而且能防止反射創建新的對象,可謂是很堅強的壁壘不過這種方式用的極少

爲什麼枚舉會滿足線程安全、序列化等標準。參考:http://blog.csdn.net/gavin_dyson/article/details/70832185


登記註冊式單例

/**
 * Created by xingyuchao on 2018/1/29.
 * 登記式單例:類似spring裏面的方法,將類名註冊,下次直接從裏面獲取
 *
 * 登記式單例實際上維護了一組單例類的實例,將這些實例存放在一個Map(登記簿)中,對於已經登記過的實例,則從Map直接獲取,對於沒有登記的,則先登記,然後返回
 *
 * 內部實現還是用的餓漢式單例,因爲其中的static方法塊,它的單例在被裝載的時候就被實例化了
 */
public class Singleton7 {

    private static Map<String,Singleton7> map = new HashMap<>();

    static{
        Singleton7 singleton7 = new Singleton7();
        map.put(singleton7.getClass().getName(),singleton7);
    }

    //保護的默認構造
    protected Singleton7(){}

    //靜態工程方法,返回此類的唯一實例
    public static Singleton7 getInstance(String name) throws Exception {
        if(name == null){
            name = Singleton7.class.getName();
        }

        if(map.get(name) == null){
            map.put(name,(Singleton7)Class.forName(name).newInstance());
        }

        return map.get(name);
    }
}


測試:

public class Test {
    
    public static void main(String[] args) throws Exception{
        //啓動100個線程同時去搶佔cpu ,有可能產生併發,觀察併發情況下是否爲同一個對象實例
        int count = 100;

        //發令槍
        CountDownLatch latch = new CountDownLatch(count);

        for (int i = 0; i < count; i++){
            //Lambda簡化後
            new Thread(()->{
                System.out.println(System.currentTimeMillis() + ":" + Singleton4.getInstance());
            }).start();

            latch.countDown();
        }
        
        latch.await(); //開始法令,搶佔cpu
    }


}
結果:


分佈式環境下的單例

有兩個問題需要注意:

1. 如果單例類由不同的類裝載器裝載,那邊可能存在多個單例類的實例。假定不是遠端存取,例如有一些servlet容器對每個servlet使用完全不同的類裝載器,這樣的話如果有兩個servlet訪問一個單例類,它們就會有各自的實例

解決:指定classloader
private static Class getClass(String classname) throws ClassNotFoundException{
        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
        
        if(classloader == null){
            classloader = Singleton.class.getClassLoader();
        }
        
        return (classloader.loadClass(classname));
    }

2. 如果Singleton實現了java.io.Serializable接口,那麼這個類的實例就可能被序列化和復原。不管怎麼樣,如果你序列化一個單例類的對象,接下來複原多個那個對象,那麼就會有多個類的實例

public class Singleton implements Serializable {
    
    public static Singleton singleton = new Singleton();
    
    protected Singleton(){}
    
    private Object readResolve(){
        return singleton;
    }
}


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