1. 程式人生 > 實用技巧 >設計模式之單例模式(Singleton Pattern)深入淺出

設計模式之單例模式(Singleton Pattern)深入淺出

單例模式介紹:單例模式是指確保一個類在任何情況下都絕對只有一個例項,並且提供一個全域性的訪問點。隱藏其所有構造方法,屬於創新型模式。

常見的單例有:ServletContext、ServletConfig、ApplicationContext、DBPool

單例模式的優點:

  • 在記憶體中只有一個例項,減少記憶體開銷。
  • 可以避免對資源的佔用
  • 設定全域性訪問點,嚴格控制訪問

單例模式的缺點:

  • 沒有介面,擴充套件困難
  • 如果要擴充套件單例物件,只有修改程式碼,沒有其他捷徑

以下是單例模式的種類及優缺點分析

餓漢式單例

在單例類首次載入時就建立例項

第一種寫法:

public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

 第二種寫法: 

public class HungryStaticSingleton {

    private static final HungryStaticSingleton hungrySingleton;

    static {
        hungrySingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
    }

    public static HungryStaticSingleton getInstance() {
        return hungrySingleton;
    }
}

  缺點:單例例項在類裝載時就構建,浪費資源空間

懶漢式單例

一、懶漢式第一種:

首先先簡單實現以下

public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySingleton = null;

    private LazySimpleSingleton() {
    }

    public static LazySimpleSingleton getInstance() {

        if (lazySingleton == null) {
            lazySingleton = new LazySimpleSingleton();
        }
        return lazySingleton;
    }
}

 我們用執行緒測一下在多執行緒場景下會不會出現問題

先建立一個執行緒類

public class ExectorTread implements Runnable {


    @Override
    public void run() {
        LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + instance);
    }
}

測試

public class LazySimpleSingletonTest {

    public static void main(String[] args) {

        Thread t1 = new Thread(new ExectorTread());
        Thread t2 = new Thread(new ExectorTread());

        t1.start();
        t2.start();

        System.out.println("Exector End");
    }
}

  執行結果

結果發現建立的物件不一樣

如果在方法上加入鎖會解決問題

public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySingleton = null;

    private LazySimpleSingleton() {
    }

    public synchronized static LazySimpleSingleton getInstance() {

        if (lazySingleton == null) {
            lazySingleton = new LazySimpleSingleton();
        }
        return lazySingleton;
    }
}

  雖然jdk1.6之後對synchronized效能優化了不少,但是還是存在一定的效能問題,這種寫法會造成整個類被鎖住,大大降低了效能

於是我們有了新的寫法

二、懶漢式第二種:
public class LazyDoubleCheckSingleton {

    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {
    }
    //    適中方案
    //    雙重檢查鎖
    public static LazyDoubleCheckSingleton getInstance() {

        if (lazySingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazySingleton;
    }
}

知識補充:

  1、執行緒安全性開發遵循三個原則:

  • 原子性:即一個操作或者多個操作要麼全部執行,要麼都不執行
  • 可見性:多個執行緒訪問同一變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看到修改的值
  • 有序性:程式執行的順序按照程式碼的先後順序執行

  通過對這段程式碼執行緒的debug發現,這裡雙重檢查體現了可見性

  2、JVM:CPU在執行的時候會轉換成JVM指令

 lazySingleton = new LazySimpleSingleton(); 這行程式碼實際進行了如下操作

  •     第一步、分配記憶體給物件
  •     第二步、初始化物件
  •     第三步、將初始化物件和記憶體地址關聯(賦值)
  •     第四步、使用者初次訪問

在多執行緒環境中,第二步和第三步可能會發生顛倒,這就需要指令重排序,於是我們在變數宣告上加上volatile關鍵字就很好的解決了問題

volatile相關部落格

三、懶漢式第三種

通過內部類的方式實現

public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton() {
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

  效能上最優的一種寫法,全程沒有用到synchronized,

  通過懶載入餓漢式寫法達到了懶漢式的目的,LazyHolder裡面的邏輯要等到外面呼叫才執行,巧妙地運用了內部類的特性

  有人會問這個不用考慮執行緒安全嗎?其實這是利用了JVM底層的執行邏輯,完美的避開了執行緒安全性的問題

但是我們會考慮另一個問題,該類構造器雖然私有了,但是還是會被反射攻擊,難逃反射法眼

我們來測試一下

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) {

        try {
//          呼叫者裝B,不走尋常路,顯然搞壞了單例
            Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
            Constructor<LazyInnerClassSingleton> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);//強吻(問)
            LazyInnerClassSingleton instance = constructor.newInstance();
            System.out.println(instance);
//          正常呼叫
            LazyInnerClassSingleton instance2 = LazyInnerClassSingleton.getInstance();
            System.out.println(instance2);
            System.out.println(instance == instance2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  執行結果

針對反射問題我們有了以下解決辦法

public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton() {
        if (LazyHolder.LAZY != null){
            throw new RuntimeException("不允許構建多個例項");
        }
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

  在私有構造方法上加上判斷,如果已經物件被初始化就丟擲異常

好的,被反射破壞的問題解決了,還會想到另一個問題,如果被反序列化物件還是單例嗎?

  知識點補充:反序列化是將已經持久的的位元組碼內容,轉換為IO流,在轉換過程中重新建立物件new。

我們拿餓漢式單例測試一下

單例類:

public class SeriableSingleton implements Serializable {

    private static final SeriableSingleton singleton = new SeriableSingleton();

    private SeriableSingleton() {
    }

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

測試類:

public class SeriableSingletonTest {

    public static void main(String[] args) {

        FileOutputStream fso = null;
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        try {
            fso = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fso);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton) ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);


        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  執行結果

顯然反序列化破壞了單例

現在我們通過原始碼尋找答案

進入readObject()方法

方法通過呼叫readObject0(false)返回結果,再次進入readObject0()方法

找到object型別,呼叫了checkResolve(readOrdinaryObject(unshared)),進入readOrdinaryObject方法

在這裡我們找到了例項化物件的語句

obj = desc.isInstantiable() ? desc.newInstance() : null;

意思是如果這個物件能被初始化就例項化物件,否則等於null

在這裡打個斷點除錯一下,確實例項化了物件,存在私有構造方法也會例項化物件

接著往下看

如果desc.hasReadResolveMethod()返回true,就呼叫Object rep = desc.invokeReadResolve(obj);返回obj

進入hasReadResolveMethod

原始碼分析過後發現這個hasReadResolveMethod()方法是用來判斷readResolve方法是否存在,如果存在返回true,不存在返回false

再看invokeReadResolve()方法

返回了readResolve這個方法的返回值,

所以經過這個判斷會重新載入物件並返回

接下來我們得出結論,程式碼增加重寫方法readResolve

public class SeriableSingleton implements Serializable {

    private static final SeriableSingleton singleton = new SeriableSingleton();

    private SeriableSingleton() {
    }

    public static SeriableSingleton getInstance() {
        return singleton;
    }

    protected Object readResolve() {
        return singleton;
    }
}

  再次執行解決了序列化的問題

但是值得我們注意的是,重寫readResolve方法只不過是覆蓋了反序列化出來的物件,物件還是建立了2次,

由於發生再JVM層面,相對來說比較安全,在之前沒有被引用的物件會被GC回收(JVM知識點)

註冊式單例

一、第一種寫法

使用列舉類實現單例模式,也是《Effictive Java》這本書推薦的寫法

public enum EnumSingleton {

    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

1、首先判斷執行緒安全性

  通過反編譯工具JAD得到列舉類的原始碼,附:JAD下載地址

public final class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(com/zc/singleton/register/EnumSingleton, name);
    }

    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    public Object getData()
    {
        return data;
    }

    public void setData(Object data)
    {
        this.data = data;
    }

    public static EnumSingleton getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingleton INSTANCE;
    private Object data;
    private static final EnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

通過程式碼發現,靜態程式碼塊例項化了單例類,屬於餓漢式單列,不存線上程安全性問題,這個例項化過程發生在JVM層面,所以可以認為懶載入

2、測試序列化

public class EnumSingletonTest {

public static void main(String[] args) { FileOutputStream fso = null; EnumSingleton s1 = null; EnumSingleton s2 = EnumSingleton.getInstance(); s2.setData(new Object()); try { fso = new FileOutputStream("EnumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fso); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(s1.getData()); System.out.println(s2.getData()); System.out.println(s1.getData() == s2.getData()); } catch (Exception e) { e.printStackTrace(); } } }

執行結果 :

列舉類是怎樣避免不被序列化破壞的呢?我們來檢視原始碼

首先進入列舉型別case

進入readEnum方法

通過列舉類物件根據註冊的類名獲取例項然後返回,所以不會建立新的物件

3、測試反射

//        反射
        try {
            Class<EnumSingleton> clazz = EnumSingleton.class;
            Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            constructor.newInstance();

        }catch (Exception e){
            e.printStackTrace();
        }

執行結果 :

報錯:沒有找到這樣的構造方法

反編譯獲取的類有這樣的一個構造方法

private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

我們用這個構造再例項化一次看看

測試:

//        反射
        try {
            Class<EnumSingleton> clazz = EnumSingleton.class;
            Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class,int.class);
            constructor.setAccessible(true);
            EnumSingleton instance = constructor.newInstance("zhou", 666);
            System.out.println(instance);

        }catch (Exception e){
            e.printStackTrace();
        }

執行結果:

報錯:不能通過反射建立這個列舉物件

檢視原始碼:

得知如果該類的型別為列舉類,就丟擲異常

總結:從JDK層面就為列舉類不被例項化和反射保駕護航

二、第二種寫法

容器式單例,Spring容器中單例的寫法

public class ContainerSingleton {

    private ContainerSingleton() {}

    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getBean(String className){

        synchronized (ioc){

            if (!ioc.containsKey(className)){
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className,obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }
            return ioc.get(className);
        }

    }
}

優點:物件方便管理,其實也是屬於懶載入

ThreadLocal

使用ThreadLocal實現單例模式

//偽執行緒安全
public class ThreadLocalSingleton {

    private ThreadLocalSingleton(){}

    private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };

    public static ThreadLocalSingleton getInstance(){
        return threadLocalSingleton.get();
    }
}

測試多執行緒

public class ExectorTread implements Runnable {


    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
    }
}
public class ThreadLocalSingletonTest {

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName() +  ":" +ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() +  ":" +ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() +  ":" +ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() +  ":" +ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() +  ":" +ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ExectorTread());
        t1.start();
        Thread t2 = new Thread(new ExectorTread());
        t2.start();
    }
}

列印結果

結論:該單例在單個執行緒中可以保持單例,但是每個其他執行緒互相都不一樣

    原理:每次獲取例項會從ThreadLocalMap中取值,而每個單例的key就是執行緒名

屬於註冊式單例(容器形式)

應用場景:Spring的orm框架中

以上對單例模式的介紹到此結束,歡迎批評指正。附:原始碼地址