天天看點

單例模式(餓漢式,懶漢式,靜态内部類模式,枚舉單例模式)

目錄

餓漢式

懶漢式

靜态内部類

強烈推薦通過枚舉實作單例模式

Java枚舉簡單介紹(了解枚舉的朋友可以跳過這部分)

枚舉單例

單例模式是 Java 中最簡單,也是最基礎,最常用的設計模式之一。在運作期間,保證某個類隻建立一個執行個體,保證一個類僅有一個執行個體,并提供一個通路它的全局通路點。下面就來講講Java中的N種實作單例模式的寫法。

餓漢式

/**
 * 餓漢式
 * 類加載到記憶體後,就執行個體化一個單例,JVM保證線程安全
 * 簡單實用,推薦使用!
 * 唯一缺點:不管用到與否,類裝載時就完成執行個體化
 * Class.forName("")
 * (話說你不用的,你裝載它幹啥)
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01() {};

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
    }
}
           

這是實作一個安全的單例模式的最簡單粗暴的寫法,這種實作方式我們稱之為餓漢式。之是以稱之為餓漢式,是因為肚子很餓了,想馬上吃到東西,不想等待生産時間。這種寫法,在類被加載的時候就把Mgr01 執行個體給建立出來了。

餓漢式的缺點就是,可能在還不需要此執行個體的時候就已經把執行個體建立出來了,沒起到lazy loading的效果。優點就是實作簡單,而且安全可靠。

懶漢式

/**
 * lazy loading
 * 也稱懶漢式
 * 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
 */
public class Mgr03 {
    private static Mgr03 INSTANCE;

    private Mgr03() {
    }
    public static Mgr03 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }
}
           

相比餓漢式,懶漢式顯得沒那麼“餓”,在真正需要的時候再去建立執行個體。在getInstance方法中,先判斷執行個體是否為空再決定是否去建立執行個體,看起來似乎很完美,但是存線上程安全問題。在并發擷取執行個體的時候,可能會存在建構了多個執行個體的情況。是以,需要對此代碼進行下改進。

/**
 * lazy loading
 * 也稱懶漢式
 * 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
 * 可以通過synchronized解決,但也帶來效率下降
 */
public class Mgr06 {
    private static volatile Mgr06 INSTANCE; //JIT

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //雙重檢查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }
           

這裡采用了雙重校驗的方式,對懶漢式單例模式做了線程安全處理。通過加鎖,可以保證同時隻有一個線程走到第二個判空代碼中去,這樣保證了隻建立 一個執行個體。這裡還用到了volatile關鍵字來修飾Mgr06 ,其最關鍵的作用是防止指令重排。

靜态内部類

/**
 * 靜态内部類方式
 * JVM保證單例(jvm保證其線程安全)
 * 加載外部類時不會加載内部類,這樣可以實作懶加載
 */
public class Singleton {

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}
           

通過靜态内部類的方式實作單例模式是線程安全的,同時靜态内部類不會在Singleton類加載時就加載,而是在調用getInstance()方法時才進行加載,達到了懶加載的效果。

似乎靜态内部類看起來已經是最完美的方法了,其實不是,可能還存在反射攻擊或者反序列化攻擊。且看如下代碼:

/**
*模拟反射攻擊
*/
public static void main(String[] args) throws Exception {
    Singleton singleton = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton newSingleton = constructor.newInstance();
    System.out.println(singleton == newSingleton);
}
           

運作結果:

單例模式(餓漢式,懶漢式,靜态内部類模式,枚舉單例模式)

通過結果看,這兩個執行個體不是同一個,這就違背了單例模式的原則了。

除了反射攻擊之外,還可能存在反序列化攻擊的情況。如下:

引入依賴:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>
           

這個依賴提供了序列化和反序列化工具類。

Singleton類實作java.io.Serializable接口。

如下:

public class Singleton implements Serializable {

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    private Singleton() {

    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        byte[] serialize = SerializationUtils.serialize(instance);
        Singleton newInstance = SerializationUtils.deserialize(serialize);
        System.out.println(instance == newInstance);
    }

}
           

運作結果:

單例模式(餓漢式,懶漢式,靜态内部類模式,枚舉單例模式)

強烈推薦通過枚舉實作單例模式

在effective java(這本書真的很棒)中說道,最佳的單例實作模式就是枚舉模式。利用枚舉的特性,讓JVM來幫我們保證線程安全和單一執行個體的問題。除此之外,寫法還特别簡單。

Java枚舉簡單介紹(了解枚舉的朋友可以跳過這部分)

枚舉的用法比較多,本文将要介紹利用枚舉實作單例模式的原理,是以這裡也主要介紹一些相關的基礎内容。

首先,枚舉類似類,一個枚舉可以擁有成員變量,成員方法,構造方法。先來看枚舉最基本的用法:

enum Type{
    A,B,C,D;
}
           

建立enum時,編譯器會自動為我們生成一個繼承自java.lang.Enum的類,我們上面的enum可以簡單看作:

class Type extends Enum{
    public static final Type A;
    public static final Type B;
    ...
}
           

對于上面的例子,我們可以把Type看作一個類,而把A,B,C,D看作類的Type的執行個體。

當然,這個建構執行個體的過程不是我們做的,一個enum的構造方法限制是private的,也就是不允許我們調用。

“類”方法和“執行個體”方法

上面說到,我們可以把Type看作一個類,而把A,B。。。看作Type的一個執行個體。同樣,在enum中,我們可以定義類和執行個體的變量以及方法。看下面的代碼:

enum Type{
    A,B,C,D;

    static int value;
    public static int getValue() {
        return value;
    }

    String type;
    public String getType() {
        return type;
    }
}

在原有的基礎上,添加了類方法和執行個體方法。我們把Type看做一個類,那麼enum中靜态的域和方法,都可以視作類方法。和我們調用普通的靜态方法一樣,這裡調用類方法也是通過  Type.getValue()即可調用,通路類屬性也是通過Type.value即可通路。
下面的是執行個體方法,也就是每個執行個體才能調用的方法。那麼執行個體是什麼呢?沒錯,就是A,B,C,D。是以我們調用執行個體方法,也就通過 Type.A.getType()來調用就可以了。

           

最後,對于某個執行個體而言,還可以實作自己的執行個體方法。再看下下面的代碼:

enum Type{
A{
    public String getType() {
        return "I will not tell you";
    }
},B,C,D;
static int value;

public static int getValue() {
    return value;
}

String type;
public String getType() {
    return type;
 }
}
           

這裡,A執行個體後面的{…}就是屬于A的執行個體方法,可以通過覆寫原本的方法,實作屬于自己的定制。

除此之外,我們還可以添加抽象方法在enum中,強制ABCD都實作各自的處理邏輯:

enum Type{
    A{
        public String getType() {
            return "A";
        }
    },B {
        @Override
        public String getType() {
            return "B";
        }
    },C {
        @Override
        public String getType() {
            return "C";
        }
    },D {
        @Override
        public String getType() {
            return "D";
        }
    };

    public abstract String getType();
}
           

枚舉單例

/**
 * 枚舉單例
 * Effective Java 作者 Joshua
 * 不僅可以解決線程同步,還可以防止反序列化。
 */
public enum Singleton {

    INSTANCE;

    public void m() {}

    public void doSomething() {
        System.out.println("doSomething");
     }
    //單例測試
    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Singleton .INSTANCE.hashCode());
            }).start();
        }
    }

}
           

調用方法:

public class Main {

    public static void main(String[] args) {
        Singleton.INSTANCE.doSomething();
    }

}
           

直接通過Singleton.INSTANCE.doSomething()的方式調用即可。友善、簡潔又安全。