懶漢模式與餓漢模式的差別及優缺點
單例模式的實作:
- 懶漢模式和餓漢模式——多線程debug
- 雙重校驗加鎖機制
- 内部類實作單例模式(延遲加載、線程安全)
- 序列化對單例模式的破壞
- 反射對單例類的攻擊
- 枚舉
- 無鎖實作單例模式
結論
What:
保證一個類隻有一個執行個體,并提供全局通路點。
Where:
- 要求生産唯一序列号。
- WEB 中的計數器,不用每次重新整理都在資料庫裡加一次,用單例先緩存起來。
- 建立的一個對象需要消耗的資源過多,比如 I/O 與資料庫的連接配接等。
懶漢模式 vs 餓漢模式
懶漢模式:很懶。在調用的時候才會建立單例。(延遲加載)
餓漢模式:很餓。在系統加載的時候就會建立單例。(預加載)
懶漢模式 | 餓漢模式 | |
---|---|---|
優點 | 第一次調用的時候才初始化,避免記憶體浪費 | 因為不用加鎖就可以保證線程安全,是以執行效率高 |
缺點 | 必須加鎖才能保證線程安全,加鎖則會影響性能 | 類加載的時候就會初始化,造成記憶體浪費 |
How:
懶漢模式和餓漢模式示例:
/**
* 懶漢模式(延遲加載,非線程安全)
*/
1. public class LazySingleton {
2.
3. private static LazySingleton lazySingleton = null;
4.
5. private LazySingleton() {
6. }
7.
8. public static LazySingleton getInstance() {
9. if (lazySingleton == null) {
10. lazySingleton = new LazySingleton();
11. }
12. return lazySingleton;
13. }
14. }
/**
* 餓漢模式(預加載,線程安全)
*/
1. public class HungrySingleton implements Serializable {
2.
3. private final static HungrySingleton hungrySingleton = new HungrySingleton();
4.
5. private HungrySingleton() {
6. }
7.
8. public static HungrySingleton getInstance() {
9. return hungrySingleton;
10. }
11. }
以上兩種代碼在單線程的情況下是沒問題的,但懶漢模式在多線程的情況就會有可能出現問題。
*在重制問題之前,首先要學會多線程debug。可以參考 idea 多線程debug
先寫一個簡單的線程類:
1. public class ThreadDemo implements Runnable{
2. @Override
3. public void run() {
4. LazySingleton lazySingleton = LazySingleton.getInstance();
5. System.out.println(Thread.currentThread().getName() + " " + lazySingleton);
6. }
7. }
1. public class Main {
2. public static void main(String[] args) {
3. new Thread(new ThreadDemo()).start();
4. new Thread(new ThreadDemo()).start();
5. System.out.println(Thread.currentThread().getName() + " " + "is Done!" );
6. }
7. }
首先在類LazySingleton中的第9行設定斷點。分别讓Thread0和Thread1到達斷點。效果如下:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwQjMx8CX39CXy8CXycXZpZVZnFWbpN0NlAXayR3cvwFduVWay9WLvRXdh9CXyI3Zv1UZnFWbp9zZwpmLiZWOlNTM0ATMkljMkZmYk1CM0cjM2MzNx8CXzV2Zh1WafRWYvxGc19CXvlmL1h2cuFWaq5ycldWYtlWLkF2bsBXdvw1LcpDc0RHaiojIsJye.jpg)
由輸出結果可以看出,多線程的情況下出現單例對象不一緻的情況。
如何寫出一個線程安全的單例模式呢?其實很簡單,使用synchronized加鎖和使用volatile防止重排序。
LazySingleton類修改如下:
public class LazySingleton {
private volatile static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
}
synchronized關鍵字可以修飾方法,也可以在方法内部作為synchronized塊。如果用synchronized修飾方法對于程式性能是有較大影響的,因為每次進入方法都會加鎖。而在方法内部特定的邏輯使用synchronized塊,靈活性較高,沒有直接用synchronized修飾方法性能的損耗大。
修改後的單例模式是否還能優化呢?接下來介紹雙重校驗加鎖機制。廢話不多說,上代碼!
/**
* 雙重校驗加鎖(延遲加載,線程安全)
*/
public class LazyDoubleCheckSingleton {
//由于會發生重排序的情況,是以使用volatile保證建立LazyDoubleCheckSingleton執行個體不會發生重排序。
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
雙重校驗加鎖機制采用雙鎖機制,安全且在多線程情況下能保持高性能。
那有沒有一種方法,既可以保證線程安全,又能延遲加載呢?
使用内部類實作單例模式(推薦使用):
public class StaticInnerClassSingleton{
private StaticInnerClassSingleton() {
}
private static class InnerClass {
private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.instance;
}
}
這種方式也是比較推薦使用的,因為實作的難度低。通過内部類實作的單例模式既可以延遲加載,不造成記憶體浪費,又可以不用加鎖保證線程安全,提高執行效率。
序列化對單例模式的破壞
到此為止,是否覺得寫的單例模式完美無缺了呢?接下來看一下的示例代碼:
public class SerializableSingleton {
private final static SerializableSingleton serializableSingleton = new SerializableSingleton();
private SerializableSingleton() {
}
public static SerializableSingleton getInstance() {
return serializableSingleton;
}
}
public class SerializableSingletonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
SerializableSingleton instance = SerializableSingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SingletonTest"));
oos.writeObject(instance);
File file = new File("SingletonTest");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
//反序列化擷取SerializableSingleton對象
SerializableSingleton newInstance = (SerializableSingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
輸出結果:
SingletonPattern.HungrySingleton@7f31245a
SingletonPattern.HungrySingleton@568db2f2
false
由輸出結果可以看出,通過序列化和反序列化之後兩個的對象是不一樣的,這也違背了單例模式的初衷。但為什麼會這樣呢?我猜測是反序列化的時候執行了什麼代碼重新建立對象導緻的。接下來通過檢視源碼去驗證我的猜測。
反序列化是通過ObjectInputStream類的readObject方法實作的,那就直接打開readObject方法撸源碼吧!
public final Object readObject() throws IOException, ClassNotFoundException{
//為省篇幅,省略部分代碼
try {
Object obj = readObject0(false);
//為省篇幅,省略部分代碼
return obj;
}
//為省篇幅,省略部分代碼
}
readObject方法傳回Object對象,那Object obj = readObject0(false);這一行代碼傳回的就是反序列化對象咯,那打開readObject0()方法檢視裡面有什麼乾坤…
/**
* Underlying readObject implementation.
*/
private Object readObject0(boolean unshared) throws IOException {
//為省篇幅,省略部分代碼
try {
switch (tc) {
//為省篇幅,省略部分代碼
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
//為省篇幅,省略部分代碼
}
}
//為省篇幅,省略部分代碼
}
由方法的注釋就知道這個方法是反序列化的實作方法。因為我傳進去的是Object對象,是以重點看return checkResolve(readOrdinaryObject(unshared))這一行。
private Object readOrdinaryObject(boolean unshared) throws IOException{
//為省篇幅,省略部分代碼
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//為省篇幅,省略部分代碼
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
由于readOrdinaryObject方法傳回的是Object對象,是以根據源碼,Object對象的聲明就在obj = desc.isInstantiable() ? desc.newInstance() : null這一行,我是不是離真相越來越近了呢?接下來繼續看isInstantiable源碼。
/**
* Returns true if represented class is serializable/externalizable and can
* be instantiated by the serialization runtime--i.e., if it is
* externalizable and defines a public no-arg constructor, or if it is
* non-externalizable and its first non-serializable superclass defines an
* accessible no-arg constructor. Otherwise, returns false.
*/
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
根據方法的注釋可知,如果一個實作了序列化的類在運作的時候被執行個體化,那就傳回true,是以就會執行desc.newInstance(),這個方法就會通過反射調用無參的構造方法建立一個新的對象。是以問題就是在這裡,通過反射傳回一個新對象。那有什麼辦法解決呢?
繼續看readOrdinaryObject方法的源碼,其中obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()就是解決問題的所在。
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
*/
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
根據方法注釋,我可以知道如果一個類是可序列化或可反序列化的,并且定義了一個方法名為readResolve就會傳回true。如果obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()判斷傳回true就會往下執行Object rep = desc.invokeReadResolve(obj)
invokeReadResolve方法通過反射的方式調用反序列化的類中定義的readResolve方法,并且指派給傳回的變量obj。
是以可以猜測,隻要在序列化、反序列化中的類定義了readResolve方法就可以解決單例模式序列化與反序列化對象不一緻的問題。
改進版SerializableSingleton類:
public class SerializableSingleton implements Serializable {
private final static SerializableSingleton serializableSingleton = new SerializableSingleton();
private SerializableSingleton() {
}
public static SerializableSingleton getInstance() {
return serializableSingleton;
}
private Object readResolve(){
return serializableSingleton;
}
}
public class SerializableSingletonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
SerializableSingleton instance = SerializableSingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SingletonTest"));
oos.writeObject(instance);
File file = new File("SingletonTest");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
SerializableSingleton newInstance = (SerializableSingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
運作結果:
SingletonPattern.SerializableSingleton@7f31245a
SingletonPattern.SerializableSingleton@7f31245a
true
小結:
在設計單例模式的時候,一定要考慮類是否需要序列化,如果需要序列化則需要添加readResolve方法傳回單例對象。
反射對單例類的攻擊
雖然上面解決了線程安全,序列化破壞單例模式的問題,但還有一個情況下,單例模式會被破壞,那就是通過反射攻擊。
在講解之前先看一下的示例:
public class ReflectTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//餓漢模式
HungrySingleton instance = HungrySingleton.getInstance();
Class reflectObject = HungrySingleton.class;
//擷取類的構造器
Constructor constructor = reflectObject.getDeclaredConstructor();
//設定在使用構造器的時候不執行權限檢查
constructor.setAccessible(true);
//通過調用無參構造函數建立對象
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
輸出結果:
SingletonPattern.HungrySingleton@4554617c
SingletonPattern.HungrySingleton@74a14482
false
由輸出結果可以看出,通過反射可以破壞單例模式。那有什麼解決辦法呢?
如果是通過内部類實作單例模式或者是餓漢模式的話,在其私有構造器上添加判斷就行。
譬如:
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
if(hungrySingleton != null){
throw new RuntimeException("單例模式禁止反射調用!");
}
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
但如果是懶漢模式,這就無法解決反射攻擊單例模式了。
枚舉
事實上,還有一種更加優雅的方式設計單例模式。就是使用枚舉!
用枚舉實作的單例模式,更簡潔,其能夠自動支援序列化機制,絕對防止多次執行個體化,還能保證線程安全。
示例:
public enum EnumSingleton {
INSTANCE;
private EnumSingleton() {
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
1. public class Test {
2. public static void main(String[] args) throws IOException, ClassNotFoundException {
3.
4. EnumSingleton instance = EnumSingleton.getInstance();
5.
6. Class reflectClass = EnumSingleton.class;
7.
8. //例子1:測試序列化、反序列化是否能破壞枚舉單例模式
9. String fileName = "SingletonTest";
10. //寫檔案
11. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName));
12. oos.writeObject(instance);
13. File file = new File(fileName);
14. //讀檔案
15. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
16. EnumSingleton newInstance = (EnumSingleton) ois.readObject();
17. System.out.println(instance == newInstance);
18.
19. //例子2:通過反射調用EnumSingleton無參構造器,測試是否能破壞枚舉單例模式
20. Constructor constructor = null;
21. try {
22. constructor = reflectClass.getDeclaredConstructor();
23. constructor.setAccessible(true);
24. EnumSingleton newInstance2 = (EnumSingleton) constructor.newInstance();
25. System.out.println(instance == newInstance2);
26. } catch (Exception e) {
27. e.printStackTrace();
28. }
29.
30. //例子3:通過反射調用EnumSingleton有參構造器,測試是否能破壞枚舉單例模式
31. Constructor constructor2 = null;
32. try {
33. constructor2 = reflectClass.getDeclaredConstructor(String.class, int.class);
34. constructor2.setAccessible(true);
35. EnumSingleton newInstance3 = (EnumSingleton) constructor2.newInstance("MuggleLee", 22);
36. System.out.println(instance == newInstance3);
37. } catch (Exception e) {
38. e.printStackTrace();
39. }
40. }
41. }
輸出結果:
true
java.lang.NoSuchMethodException: SingletonPattern.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at SingletonPattern.SerializableSingletonTest.main(SerializableSingletonTest.java:22)
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at SingletonPattern.SerializableSingletonTest.main(SerializableSingletonTest.java:35)
首先看下例子1,輸出結果是true,可以證明,序列化反序列化對枚舉單例沒影響。
接下來看例子2,輸出結果報錯顯示java.lang.NoSuchMethodException。這是因為,枚舉類實際上是會繼承抽象類Enum,而Enum類隻有一個有參構造器 protected Enum(String name, int ordinal),是以通過constructor.newInstance()調用無參構造器是錯誤的。
那例子3是調用有參構造器,為什麼還會報錯呢?看報錯資訊已經很清晰了,Cannot reflectively create enum objects,不能通過反射建立枚舉對象。
通過例子可以證明,使用枚舉類可以保證不會被序列化破壞,還能保證不會受到反射攻擊的影響。那為什麼還能保證線程安全呢?
這和JVM類加載機制有關,static類型的屬性會在類被加載之後被初始化,當一個Java類第一次被真正使用到的時候靜态資源被初始化、由于虛拟機在加載枚舉的類的時候,會使用ClassLoader的loadClass方法,而這個方法使用關鍵字synchronized同步代碼塊保證了線程安全,是以Java類的加載和初始化過程都是線程安全的。
無鎖實作單例模式
以上的幾種建立單例模式都是通過synchronized鎖實作的,那有沒有其他方法,不使用鎖又能保證線程安全呢?
我們可以使用CAS實作單例模式!
public class SingletonByCAS {
private static AtomicReference<SingletonByCAS> INSTANCE = new AtomicReference();
public SingletonByCAS() {
}
public static SingletonByCAS getInstance() {
SingletonByCAS singletonByCAS = INSTANCE.get();
for (; ; ) {
if (singletonByCAS != null) {
return singletonByCAS;
}
singletonByCAS = new SingletonByCAS();
if (INSTANCE.compareAndSet(null, singletonByCAS)) {
return singletonByCAS;
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
SingletonByCAS singletonByCAS = SingletonByCAS.getInstance();
System.out.println(singletonByCAS);
}).start();
}
}
}
輸出結果:
SingletonPattern.SingletonByCAS@2fd04fd1
SingletonPattern.SingletonByCAS@2fd04fd1
...
...
...
SingletonPattern.SingletonByCAS@2fd04fd1
SingletonPattern.SingletonByCAS@2fd04fd1
通過使用AtomicXXX包裝類調用compareAndSet方法可以保證線程安全,但通過這種方式實作的單例模式真的好嗎?
使用CAS的好處是不需要通過鎖的方式保證線程安全,但是缺點也很顯然,因為是通過自旋的方式執行,如果一直循環或者執行速度很慢的話,CPU的開銷會非常大。
雖然不推薦這種方式實作單例模式,但也可以更加了解CAS的實作和運用。
結論:
設計單例模式盡量選擇用枚舉的方式,代碼量不大而且能保證線程安全、不會遭到序列化、反射的破壞。
了解更多設計模式:
設計模式系列