單例模式可以說只要是一個合格的開發都會寫,但是如果要深究,小小的單例模式可以牽扯到很多東西,比如 多執行緒是否安全,是否懶載入,效能等等。還有你知道幾種單例模式的寫法呢?如何防止反射破壞單例模式?今天,我就花一章內容來說說單例模式。
關於單例模式的概念,在這裡就不在闡述了,相信每個小夥伴都瞭如指掌。
我們直接進入正題:
餓漢式
public class Hungry {
private Hungry() {
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance() {
return hungry;
}
}
複製程式碼
餓漢式是最簡單的單例模式的寫法,保證了執行緒的安全,在很長的時間裡,我都是餓漢模式來完成單例的,因為夠簡單,後來才知道餓漢式會有一點小問題,看下面的程式碼:
public class Hungry {
private byte[] data1 = new byte[1024];
private byte[] data2 = new byte[1024];
private byte[] data3 = new byte[1024];
private byte[] data4 = new byte[1024];
private Hungry() {
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance() {
return hungry;
}
}
複製程式碼
在Hungry類中,我定義了四個byte陣列,當程式碼一執行,這四個陣列就被初始化,並且放入記憶體了,如果長時間沒有用到getInstance方法,不需要Hungry類的物件,這不是一種浪費嗎?我希望的是 只有用到了 getInstance方法,才會去初始化單例類,才會載入單例類中的資料。所以就有了 第二種單例模式:懶漢式。
懶漢式(DCL)
public class LazyMan {
private LazyMan() {
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
複製程式碼
DCL懶漢式的單例,保證了執行緒的安全性,又符合了懶載入,只有在用到的時候,才會去初始化,呼叫效率也比較高,但是這種寫法在極端情況還是可能會有一定的問題。因為
lazyMan = new LazyMan();
複製程式碼
不是原子性操作,至少會經過三個步驟:
- 分配記憶體
- 執行構造方法
- 指向地址
由於指令重排,導致A執行緒執行 lazyMan = new LazyMan();的時候,可能先執行了第三步(還沒執行第二步),此時執行緒B又進來了,發現lazyMan已經不為空了,直接返回了lazyMan,並且後面使用了返回的lazyMan,由於執行緒A還沒有執行第二步,導致此時lazyMan還不完整,可能會有一些意想不到的錯誤,所以就有了下面一種單例模式。
懶漢式(Volatile)
這種單例模式只是在上面DCL單例模式增加一個volatile關鍵字來避免指令重排:
public class LazyMan {
private LazyMan() {
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
複製程式碼
持有者
public class Holder {
private Holder() {
}
public static Holder getInstance() {
return InnerClass.holder;
}
private static class InnerClass {
private static final Holder holder = new Holder();
}
}
複製程式碼
這種方式是第一種餓漢式的改進版本,同樣也是在類中定義static變數的物件,並且直接初始化,不過是移到了靜態內部類中,十分巧妙。既保證了執行緒的安全性,同時又滿足了懶載入。
萬惡的反射
萬惡的反射登場了,反射是一個比較霸道的東西,無視private修飾的構造方法,可以直接在外面newInstance,破壞我們辛辛苦苦寫的單例模式。
public static void main(String[] args) {
try {
LazyMan lazyMan1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan2.hashCode());
System.out.println(lazyMan1 == lazyMan2);
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
我們分別列印出lazyMan1,lazyMan2的hashcode,lazyMan1是否相等lazyMan2,結果顯而易見:
那麼,怎麼解決這種問題呢?
public class LazyMan {
private LazyMan() {
synchronized (LazyMan.class) {
if (lazyMan != null) {
throw new RuntimeException("不要試圖用反射破壞單例模式");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
複製程式碼
在私有的建構函式中做一個判斷,如果lazyMan不為空,說明lazyMan已經被建立過了,如果正常呼叫getInstance方法,是不會出現這種事情的,所以直接丟擲異常:
但是這種寫法還是有問題:
上面我們是先正常的呼叫了getInstance方法,建立了LazyMan物件,所以第二次用反射建立物件,私有建構函式裡面的判斷起作用了,反射破壞單例模式失敗。但是如果破壞者乾脆不先呼叫getInstance方法,一上來就直接用反射建立物件,我們的判斷就不生效了:
public static void main(String[] args) {
try {
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan2.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
那麼如何防止這種反射破壞呢?
public class LazyMan {
private static boolean flag = false;
private LazyMan() {
synchronized (LazyMan.class) {
if (flag == false) {
flag = true;
} else {
throw new RuntimeException("不要試圖用反射破壞單例模式");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
複製程式碼
在這裡,我定義了一個boolean變數flag,初始值是false,私有建構函式裡面做了一個判斷,如果flag=false,就把flag改為true,但是如果flag等於true,就說明有問題了,因為正常的呼叫是不會第二次跑到私有構造方法的,所以丟擲異常:
看起來很美好,但是還是不能完全防止反射破壞單例模式,因為可以利用反射修改flag的值。
看起來並沒有一個很好的方案去避免反射破壞單例模式,所以輪到我們的列舉登場了。
列舉
public enum EnumSingleton {
instance;
}
複製程式碼
列舉是目前最推薦的單例模式的寫法,因為足夠簡單,不需要開發自己保證執行緒的安全,同時又可以有效的防止反射破壞我們的單例模式,我們可以看下newInstance的原始碼:
重點就是紅框中圈出來的部分,如果列舉去newInstance就直接丟擲異常了。好了,這章的內容就結束了,下次再有人問你單例模式,再也不用害怕了。