1. 程式人生 > >Spring中常見的設計模式——單例模式

Spring中常見的設計模式——單例模式

一、單例模式的應用場景

  單例模式(singleton Pattern)是指確保一個類在任何情況下都絕對只有一個例項,並提供一個全域性訪問點。J2EE中的ServletContext,ServletContextConfig等;Spring中的ApplicationContext、資料庫連線池等。

二、餓漢式單例模式

  餓漢式單例模式在類載入的時候就立即初始化,並且建立單例物件。它是絕對的執行緒安全、線上程還沒出現以前就實現了,不可能存在訪問安全問題。

  優點:沒有增加任何鎖,執行效率高,使用者體驗比懶漢式好。

  缺點:類載入的時候就初始化了,用不用都進行,浪費記憶體。

  Spring 中IoC容器ApplocationContext本身就是典型的餓漢式單例模式:

public class HungrySingleton {
    private static final HungrySingleton h = new HungrySingleton();

    private HungrySingleton() {
    }

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

  餓漢式單例模式適用於單例物件較少的情況。

三、懶漢式單例模式

  被外部呼叫才會載入:

public class LazySimpleSingleton {
    private LazySimpleSingleton() {
    }

    private static LazySimpleSingleton lazy = null;

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

利用執行緒建立例項:

public class ExectorThread implements Runnable {
    @Override
    public void run() {
        LazySimpleSingleton simpleSingleton = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + simpleSingleton);
    }
}

客戶端程式碼:

public class LazySimpleSingletonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("END");
    }
}

結果:

END
Thread-1:singleton.Lazy.LazySimpleSingleton@298c37fd
Thread-0:singleton.Lazy.LazySimpleSingleton@6ebc1cfd

可以看到 產生的兩個例項的記憶體地址不同說明產生了兩個例項,大家可以通過以下打斷點的方式實現不同Thread執行狀態見進行切換。

  要解決執行緒問題第一反應是加 synchronized 加在建立例項的地方:public static synchronized LazySimpleSingleton getInstance(),但當執行緒數量較多時,用Synchronized加鎖,會使大量執行緒阻塞,就需要更好的解決辦法:

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

  synchronized (lock) lock這個物件就是 “鎖”,當兩個並行的執行緒a,b,當a先進入同步塊,即a先拿到lock物件,這時候a就相當於用一把鎖把synchronized裡面的程式碼鎖住了,現在只有a才能執行這塊程式碼,而b就只能等待a用完了lock物件鎖之後才能進入同步塊。但是用到 synchronized 總歸是要上鎖的,對效能還是有影響,那就用這種方式:用內部類的方式進行懶載入。

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton() {
    }

    private static final LazyInnerClassSingleton getIngestance() {
        return LazyHolder.LAZY;
    }

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

  內部類在LazyInnerClassSingleton類載入時載入,解決了餓漢式的效能問題,LazyInnerClassSingleton在內部類載入時,getIngestance()方法被呼叫之前例項化,解決了執行緒不安全問題。

 四、反射破壞單例

public class LazyInnerClassSingletonTest {
    public static void main(String[] args) {
        try {
            Class<?> clazz = LazyInnerClassSingleton.class;
            //通過反射回去私有構造方法
            Constructor constructor = clazz.getDeclaredConstructor(null);
            //強制訪問
            constructor.setAccessible(true);
            //暴力初始化
            Object o1 = constructor.newInstance();
            //建立兩個例項
            Object o2 = constructor.newInstance();
            System.out.println("o1:" + o1);
            System.out.println("o2:" + o2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

結果:

o1:singleton.Lazy.LazyInnerClassSingleton@1b6d3586
o2:singleton.Lazy.LazyInnerClassSingleton@4554617c

建立了兩個例項,違反了單例,現在在構造方法中做一些限制,使得多次重複建立時,丟擲異常:

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

這應該就是最好的單例了,哈哈哈。

五、註冊式單例模式

  註冊式單例模式又稱為登記式單例模式,就是將每個例項都登記到某個地方,使用唯一標識獲取例項。註冊式單例模式有兩種:列舉式單例模式、容器式單例模式。註冊式單例模式主要解決通過反序列化破壞單例模式的情況。

1.列舉式單例模式 

public enum EnumSingleton {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

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

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

測試程式碼:

public class EnumSingletonTest {
    public static void main(String[] args) {
        try {
            EnumSingleton instance1 = EnumSingleton.getInstance();
            EnumSingleton instance2 = null;
            instance1.steData(new Object());

            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance1);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance2 = (EnumSingleton) ois.readObject();
            ois.close();

            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

結果:

java.lang.Object@568db2f2
java.lang.Object@568db2f2

  那列舉式單例是如何解決反序列化得問題呢?

  通過反編譯,可以在EnumSingleton.jad檔案中發現static{} 程式碼塊,列舉式單例模式在靜態程式碼塊中給INSTANCE進行了賦值,是餓漢式單例模式的實現。檢視JDK原始碼可知,列舉型別其實通過類名和類物件找到一個唯一的列舉物件。因此,列舉物件不可能被類載入器載入多次。

  當你試圖用反射破壞單例時,會報 Cannot reflectively create enum objects ,即不能用反射來建立列舉型別。進入Customer的newInstance(),其中有判斷:如果修飾符是Modifier.ENUM,則直接丟擲異常。JDK列舉的語法特殊性及反射也為美劇保駕護航,讓列舉式單例模式成為一種比較優雅的實現。

2.容器式單例

public class ContainerSingleton {
    private ContainerSingleton() {
    }

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

    public static Object getBean(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object o = null;
                try {
                    o = Class.forName(className).newInstance();
                    ioc.put(className, o);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return o;
            } else {
                return ioc.get(className);
            }
        }
    }
}

spring中使用的就是容器式單例模式。

  

 &n