目錄
餓漢式
懶漢式
靜态内部類
強烈推薦通過枚舉實作單例模式
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()的方式調用即可。友善、簡潔又安全。