Java設計模式——單例模式

程式猿開心發表於2019-05-26

Java設計模式——單例模式

我一直覺得,設計模式的思想都是源於生活的。單例在生活之中也是經常用到的,比如國家領導人、某某公司創始人......類似這種獨一無二的。單例模式也屬於建立型設計模式,確保在任何情況下單例類最多隻能有一個例項物件,並且提供全域性訪問點。單例模式可以保證記憶體裡只有一個例項,減少了記憶體開銷;可以避免對資源的多重佔用。反正就是在記憶體世界裡,單例模式的類的例項是獨一無二的。當然也有執行緒單例,就是同一個執行緒只有一個單例類例項。

餓漢式單例

類載入的時候就立馬初始化生成類的例項物件。不管來沒來,先吃飽再說。

步驟:

  1. 提供一個單例類的私有的最終的靜態全域性單例類屬性變數。
  2. 私有化單例類的構造方法。
  3. 初始化物件(可以在第一步或者第二步完成)。
  4. 提供全域性訪問點,用於返回物件實現。

程式碼:

/**
 * @description: 餓漢式單例
 * @author: lmc
 * @create: 2019-04-02 20:39
 **/

public class HungrySingletonOne implements Serializable {

    /**
     * 餓漢式單例,在類初始化的時候就進行物件的建立,不存在不同步問題
     */

    //第一步提供一個私有的最終的靜態全域性屬性變數,用於返回物件。
    private static final HungrySingletonOne hungrySingletonOne=new HungrySingletonOne();

    //第二步 私有化構造方法
    private HungrySingletonOne(){
        if(null != hungrySingletonOne){
            throw new RuntimeException("單例類,不允許被反射例項化");
        }
    }

    //提供全域性訪問點
    public static HungrySingletonOne getInstance(){
        return hungrySingletonOne;
    }

    /**
     * @description: 重寫readResolve方法,防止序列化破壞單例
     * @return java.lang.Object
     * @date 2019/5/25 22:12
     * @author lmc
     */
    private Object readResolve(){
        return hungrySingletonOne;
    }
}
/**
 * @description: 餓漢式單例
 * @author: lmc
 * @create: 2019-04-02 20:39
 **/

public class HungrySingletonTwo {

    /**
     * 餓漢式單例,在類初始化的時候就進行物件的建立
     */

    //第一步提供一個私有的最終的靜態全域性屬性變數,用於返回物件。
    private static final HungrySingletonTwo hungrySingletonTwo;

    static {
        hungrySingletonTwo=new HungrySingletonTwo();
    }

    //第二步 私有化構造方法
    private HungrySingletonTwo(){
        if(null != hungrySingletonTwo){
            throw new RuntimeException("單例類,不允許被反射例項化");
        }
    }

    //提供全域性訪問點
    public static HungrySingletonTwo getInstance(){
        return hungrySingletonTwo;
    }

    /**
     * @description: 重寫readResolve方法,防止序列化破壞單例
     * @return java.lang.Object
     * @date 2019/5/25 22:12
     * @author lmc
     */
    private Object readResolve(){
        return hungrySingletonTwo;
    }
}

餓漢式單例利弊

利:沒有加鎖,執行效率高,比懶漢式體驗好。餓漢式,在單例類載入的時候就已經初始化好了例項物件,不存線上程安全問題,因此沒有共享的說法。

弊:如果單例類不經常使用,佔用了記憶體。

Spring中 IOC容器ApplicationContext本身就是典型的餓漢式單例

餓漢式單例不存線上程安全問題,這裡就不做測試結果展示了,上面的程式碼都是可以直接執行測試的。

懶漢式單例之雙重檢查鎖單例

懶漢式單例類在類載入的時候不會初始化類生成單例類的例項,而是在呼叫單例類獲取例項的方法的時候才會去初始化例項物件,並返回一個單例物件。

因為懶漢式單例,是在單例類的獲取例項方法被呼叫的時候才會去初始化物件,所以存在高併發,執行緒安全問題。

synchronized用來保證執行緒安全問題(原理在這裡不細說了)

根據業務需求,synchronized關鍵字能不寫在方法上就不要寫在方法上,寫在方法裡面。這樣可以避免整個類都被鎖住,寫在方法裡面,其他執行緒還是能夠執行這個方法被鎖之前的程式碼的。效能稍微提供提高一點點。所以我們有了雙重檢查鎖單例。

/**
 * @description: 簡單的懶漢式單例
 * @author: lmc
 * @create: 2019-04-03 08:59
 **/
public class LazySimpleSingleton implements Serializable {

    //第一步 構造方法私有化,並且設定異常防止反射破壞單例
    private LazySimpleSingleton(){
        if(null != lazySimpleSingleton){
            throw new RuntimeException("單例類,不允許被反射例項化");
        }
    }
    //第二步 定義物件屬性
    private static volatile LazySimpleSingleton lazySimpleSingleton=null;

    //第三步 宣告全域性訪問點
    /**
     * @description: 雙重檢查鎖單例
     * @return com.lmc.gp12380.pattern.singleton.lazy.LazySimpleSingleton
     * @date 2019/5/25 21:21
     * @author lmc
     */
    public static LazySimpleSingleton getInstance(){

        if(null == lazySimpleSingleton){//第一次檢查
            synchronized (LazySimpleSingleton.class){//加鎖 保證執行緒安全性
                if(null == lazySimpleSingleton){//第二次檢查
                    lazySimpleSingleton=new LazySimpleSingleton();
                }
            }
        }
        return lazySimpleSingleton;
    }

    /**
     * @description: 重寫readResolve方法,防止序列化破壞單例
     * @return java.lang.Object
     * @date 2019/5/25 22:12
     * @author lmc
     */
    private Object readResolve(){
        return lazySimpleSingleton;
    }
}

雙重檢查鎖單例,可以用 ideadebug執行緒模式除錯測試。

volatile關鍵字的作用在這裡不細說,就是用來保證絕對的執行緒安全。

定義一個執行緒執行器

/**
 * @description: 執行執行緒
 * @author: lmc
 * @create: 2019-04-03 08:56
 **/
public class ExectorThread implements Runnable {

    public void run() {
        LazySimpleSingleton lazySimpleSingleton=LazySimpleSingleton.getInstance();
        System.out.println("執行緒"+Thread.currentThread().getName()+"lazySimpleSingleton"+lazySimpleSingleton);
    }
}

測試程式碼

/**
 * @description: 簡單懶漢式單例測試
 * @author: lmc
 * @create: 2019-04-03 09:10
 **/
public class LazySimpleSingletonTest {

    public static void main(String[] args){
        
        for (int i = 0; i <10; i++) {
            Thread thread= new Thread(new ExectorThread());
            thread.start();
        }
    }
}

測試結果Java設計模式——單例模式

可以看到,10個執行緒獲取的物件都是同一個例項。

懶漢式單例之靜態內部類單例

靜態內部類單例在外部類呼叫獲取例項方法的時候才會初始化例項物件,靜態內部類在類載入的時候並不會初始化,只有在建立內部類物件或者,內部類物件靜態成員被第一次引用的時候才會初始化物件。然而,對於靜態內部類單例來說,我們永遠不會主動的去建立內部類物件。

/**
 * @description: 懶漢式靜態內部類單例
 * @author: lmc
 * @create: 2019-04-03 11:28
 **/
public class LazyInnerClassSingleton implements Serializable {

    //私有化構造方法
    private LazyInnerClassSingleton(){
        if(Holder.lazy != null){//只能呼叫一次構造建立例項
            throw new RuntimeException("靜態內部類單例,不允許建立多個例項");
        }
    }

    public static final LazyInnerClassSingleton getInstance(){
        return Holder.lazy;
    }

    /**
     * @description:靜態內部類初始化外部類成員變數
     * @date 2019/5/25 22:11
     * @author lmc
     */
    private static class Holder {
        private final static LazyInnerClassSingleton lazy=new LazyInnerClassSingleton();
    }

    /**
     * @description: 重寫readResolve方法,防止序列化破壞單例
     * @return java.lang.Object
     * @date 2019/5/25 22:12
     * @author lmc
     */
    private Object readResolve(){
        return Holder.lazy;
    }
}

上面的程式碼中,內部類Holder的靜態成員變數 lazy是 final static修飾,無論是建立內部類物件,初始化lazy還是呼叫靜態屬性lazy引用初始化都之後初始化一次。並且LazyInnerClassSingleton單例類只能被內部類例項化一次。

測試程式碼

/**
 * @description: 內部類懶漢式單例測試
 * @author: lmc
 * @create: 2019-04-03 09:10
 **/

public class LazyInnerClassSingletonTest {


    public static void main(String[] args){

        for (int i = 0; i <10 ; i++) {
            Thread thread= new Thread(new Runnable() {
                @Override
                public void run() {
                    LazyInnerClassSingleton lazyInnerClassSingleton =   LazyInnerClassSingleton.getInstance();
                    System.out.println("執行緒"+Thread.currentThread().getName()+"lazyInnerClassSingleton"+lazyInnerClassSingleton);
                }
            });
            thread.start();
        }
    }
}

測試結果
Java設計模式——單例模式

註冊式單例之容器單例

/**
 * @description: 容器式單例
 * @author: lmc
 * @create: 2019-04-08 21:43
 **/
public class ContainerSingleton {

    private ContainerSingleton(){};

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

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

如果需要建立很多單例物件,一般用容器式單例管理物件。

雖然 ConcurrentHashMap是執行緒安全的,但是呼叫getBean方法不是執行緒安全的,所有要加synchronized鎖。

容器式單例就不寫測試結果了。

註冊式單例之列舉單例

/**
 * @description: 列舉單例
 * @author: lmc
 * @create: 2019-04-03 15:31
 **/

public enum EnumSingleton {

    INSTENCE;

    private EnumSingleton(){

    }

    private Object data;

    public Object getData() {
        return data;
    }

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

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

列舉型別重JVM虛擬機器底層就幫我們做了防止序列化和反射破壞單例。

反編譯EnumSingleton.class檔案

package com.lmc.gp12380.pattern.singleton.register;


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/lmc/gp12380/pattern/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 INSTENCE;
    }

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

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

反編譯之後列舉 INSTENCE變成了靜態最終變數,由靜態程式碼塊餓漢式初始化,EnumSingleton建構函式也是私有的,不允許外部建立物件。所有列舉符合單例需求。

測試程式碼

/**
 * @description: 列舉單例測試
 * @author: lmc
 * @create: 2019-04-03 15:33
 **/

public class EnumSingletonTest {

    public static void main(String[] args) {

        for (int i = 0; i <10 ; i++) {
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    EnumSingleton enumSingleton=EnumSingleton.getInstance();
                    System.out.println(Thread.currentThread().getName()+"enumSingleton>>"+enumSingleton);
                }
            });
            thread.start();
        }
    }
}

測試結果
Java設計模式——單例模式

反射和序列化破壞單例測試

上述程式碼中,餓漢式和懶漢式,在私有化構造方法中都是有條件丟擲異常

if(condition){
    throw new RuntimeException("單例類,不允許被反射例項化");
}

這段程式碼是為了保證單例類只能例項化一次,防止反射破壞單例。

 private Object readResolve(){
    return hungrySingletonOne;
 }

上面的程式碼是重寫了Serializable介面的readResolve方法,是為了防止序列化破壞單例物件。

防止反射破壞單例測試

/**
 * @description: 反射破壞單例測試
 * @author: lmc
 * @create: 2019-05-25 23:19
 **/

public class TestReflectDestructionSingleton {

    public static void main(String[] args) {

        try {
            LazyInnerClassSingleton lazyInnerClassSingleton1= LazyInnerClassSingleton.getInstance();
            System.out.println(lazyInnerClassSingleton1);
            Class<?> clazz=LazyInnerClassSingleton.class;
            Constructor c=clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            LazyInnerClassSingleton lazyInnerClassSingleton2= (LazyInnerClassSingleton) c.newInstance();
            System.out.println(lazyInnerClassSingleton2);
            System.out.println(lazyInnerClassSingleton1==lazyInnerClassSingleton2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

測試結果
Java設計模式——單例模式

利用反射生成例項,直接丟擲異常,中斷程式執行。客戶端呼叫就不會去利用反射了。

去掉上面的丟擲異常的條件執行測試程式,會出現兩個不一樣的例項,單例被破壞。

防止序列化破壞

/**
 * @description: 序列化破壞單例測試
 * @author: lmc
 * @create: 2019-05-25 23:13
 **/

public class TestSerializDestructionSingleton {

    public static void main(String[] args) {

        LazyInnerClassSingleton s1 = null;
        LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("LazyInnerClassSingleton.txt");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();
            FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.txt");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (LazyInnerClassSingleton)ois.readObject();
            ois.close();
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

測試結果
Java設計模式——單例模式

序列化和反序列化生成的物件和之前的物件是一樣的,這就說明單例模式有效。

驗證序列化破壞單例只需要去掉重寫的readResolve方法就可以得到兩個不一樣的例項,單例被破壞。

反射破壞單例測試和序列化破壞單例只需要更換類就能測試其他單例了,在這就不做測試了。

註冊式容器式單例是從Map集合獲取物件,不需要做單例破壞測試。

註冊式列舉式單例是重JVM層面防止單例破壞。雖然沒有加上面的防止破壞程式碼,也可以用上面的測試程式碼測試。

相關文章