單例模式可以說只要是一個合格的開發都會寫,但是如果要深究,小小的單例模式可以牽扯到很多東西,比如 多線程是否安全,是否懶加載,性能等等。還有你知道幾種單例模式的寫法呢?如何防止反射破壞單例模式?今天,我就花一章內容來說說單例模式。
關於單例模式的概念,在這里就不在闡述了,相信每個小伙伴都了如指掌。
我們直接進入正題:
餓漢式
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就直接拋出異常了。
好了,這章的內容就結束了,下次再有人問你單例模式,再也不用害怕了。