天天看點

單例模式

單例模式的定義如下:

確定某一個類隻有一個執行個體,而且自行執行個體化并向整個系統提供這個執行個體。

單例類自身儲存它的唯一執行個體,這個類保證沒有其他執行個體可以被建立,并且它可以提供一個通路該執行個體的方法。

單例模式的一些特點:

構造方法私有化,防止外部通過通路構造方法建立對象;

提供一個全局方法使其單例對象被外部通路;

考慮多線程并發情況的單例唯一性。

單例模式的幾種實作方式:

懶漢式、餓漢式、内部類、注冊式、枚舉類

這裡強烈推薦的是内部類和枚舉類的實作方式。

餓漢式

在類加載的時候就立即建立單例對象。

  • 優點:絕對的線程安全,無鎖,執行效率高;
  • 缺點:即使單例在程式中一直用不到,也會在類加載的時候初始化,不管用或不用,都占據記憶體空間。
package com.faith.net;

/**
 * 餓漢式
 */
public class Singleton {

    private Singleton(){}

    private static final Singleton hungry = new Singleton();

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

懶漢式

當需要使用單例的時候才進行執行個體化。

package com.faith.net;

/**
 * 懶漢式
 */
public class Singleton {

    private Singleton(){}
    
    private static Singleton lazy = null;

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

需要注意的是以上擷取單例不是線程安全的。

  • 用synchronized改版
package com.faith.net;

/**
 * 懶漢式
 */
public class Singleton {

    private Singleton(){}

    private static Singleton lazy = null;

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

通過synchronized關鍵字保證了線程安全,但這種排隊方式的處理在多線情況下效率較低。

雙重鎖定

雙重鎖定是懶漢式的一種實作方式,上面例子中synchronized關鍵字加在了方法上,導緻了每個線程都會排隊擷取對象。

雙重鎖定的方式是不讓線程每次都加鎖,而是隻在執行個體未被建立的情況下加鎖,同時也能保證線程安全,這種做法稱為雙重鎖定(Double-Check Locking)。

示例如下:

package com.faith.net;

import java.io.Serializable;

/**
 * 單例類
 */
public class Singleton implements Serializable{
    
    private volatile static Singleton singleton;
    
    private Singleton (){}
    
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
    private Object readResolve() {
        return singleton;
    }
}           

内部類

這種方式使用了内部類的一些特性:

  • 内部類隻有在外部類被調用的時候才會被加載;
  • 内部類在方法調用之前完成初始化;
  • 其次,類的加載機制保證了每個類隻會被加載一次。

這是非常推薦的一種方式,它綜合了懶漢式延遲加載和餓漢式線程安全的特性。

package com.faith.net;

/**
 * 内部類方式
 */
public class Singleton {

    private boolean initialized = false;
    
    private Singleton(){}

    /*
     * static 保證單例共享,final保證方法不被重寫
     */
    public static final Singleton getInstance(){
        return LazyHolder.LAZY; // 在傳回結果以前,會先加載内部類
    }

    // 内部類
    private static class Singleton{
        private static final Singleton LAZY = new Singleton();
    }
}           

注冊式

注冊式是spring IOC容器使用的一種方式,通過将執行個體儲存到Map容器中實作。

package com.faith.net;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 注冊式
 */
public class BeanFactory {

    private BeanFactory(){}
    
    private static Map<String,Object> container = new ConcurrentHashMap<String,Object>();

    public static synchronized Object getBean(String className){
        if(!ioc.containsKey(className)){
            Object obj = Class.forName(className).newInstance();
            container.put(className,obj);
            return obj;
        }else{
            return container.get(className);
        }
    }
}           

枚舉類

枚舉有如下特性:

  • 枚舉對象,例如下面的INSTANCE由枚舉機制保證了一定會是單例的;
  • 枚舉的加載機制保證了線程安全;
  • 枚舉保證了執行個體不會被反射破壞;
  • 枚舉的對象隻有被使用時才會進行執行個體化,保證了延遲加載。

是以通過枚舉實作,也會保證代碼的高效、線程安全以及延遲加載的特性。

實作方式
  • 傳回自身對象
package com.faith.net;

public enum Singleton {

    INSTANCE; // 單例對象

    // 這裡可以添加多個成員方法,例如
    public void doSomething() {
        // 省略具體代碼
    }
}           
  • 傳回其他對象,例如ConcurrentHashMap
package com.faith.net;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public enum Singleton {

    INSTANCE; // 枚舉對象,由于枚舉機制,這個也是單例的

    private Map instance; // 需要做成單例的對象

    Singleton() {
        instance = new ConcurrentHashMap();
    }

    public Map getInstance() {
        return instance;
    }
}           

反序列化對單例的破壞

對象的序列化指将對象轉化為位元組流;反序列化指将位元組流轉化為相應的對象。

反序列化的特點是,預設情況下,會根據位元組流建立一個新的對象。那麼這種特性會導緻破壞單例。如下:

  • 建立單例類
package com.faith.net;

import java.io.Serializable;

/**
 * 單例類
 */
public class Singleton implements Serializable{
    
    private volatile static Singleton singleton;
    
    private Singleton (){}
    
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}           
  • 用戶端測試反序列化
package com.faith.net;

import java.io.*;

public class SerializableDemo1 {

    public static void main(String[] args) throws Exception {
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(Singleton.getSingleton());
        
        // 反序列化
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        Singleton newInstance = (Singleton) ois.readObject();
        
        // 判斷
        System.out.println(newInstance == Singleton.getSingleton());
    }
}           

輸出會為false。這種情況可以通過在單例類中添加readResolve方法解決,如下:

package com.faith.net;

import java.io.Serializable;

/**
 * 單例類
 */
public class Singleton implements Serializable{
    
    private volatile static Singleton singleton;
    
    private Singleton (){}
    
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
    private Object readResolve() {
        return singleton;
    }
}           

因為java對象的反序列化通過ObjectInputputStream實作的,通過readObject方法讀取位元組流并建立新的對象。

但在readObject方法中有一個判斷,内容是判斷被反序列化的類中是否包含readResolve方法,如果包含readResolve方法,就直接傳回readResolve方法中的邏輯,是以readResolve能避免反序列化對單例模式的破壞。

反射對單例的破壞

懶漢式、餓漢式都可以被反射破壞,如下:

public static void main(String[] args) {
        Class<?> clazz = Singleton.class;

        //通過反射拿到私有的構造方法
        Constructor c = clazz.getDeclaredConstructor(null);
        //強制通路私有構造
        c.setAccessible(true);
        
        //調用了兩次構造方法,相當于new了兩次
        Object o1 = c.newInstance();
        Object o2 = c.newInstance();
        
        System.out.println(o1 == o2);
}           

而枚舉可以避免被反射破壞,因為枚舉不能被反射方式通路。

繼續閱讀