本文參考了以下博客
https://www.cnblogs.com/xiaobai1226/p/8487696.html
https://blog.csdn.net/qq_35860138/article/details/86477538
https://blog.csdn.net/li295214001/article/details/48135939/
單例模式是一種對象創建型模式,使用單例模式,可以保證爲一個類只生成唯一的一個實例對象。也就是說,在整個程序空間中,該類只存在一個實例對象。
GOF對單例模式的定義是:保證一個類,只有一個實例存在,同時提供能對該實例加以訪問的全局訪問方法。
單例具有以下基本特點
- 聲明靜態私有類變量,且立即實例化,保證實例化一次
- 私有構造,防止外部實例化
- 提供public的
getInstance()
方法供外部獲取單例實例
1、懶漢式
特點:懶加載,在實例被調用時才初始化。但是線程不安全。
public class LazyInstance {
private static LazyInstance instance = null;
/**
* 私有化構造函數
*/
private LazyInstance() {
}
/**
* 提供一個全局的靜態方法
*/
public static LazyInstance getInstance() {
if (instance == null) {
instance = new LazyInstance();
}
return instance;
}
}
2、加鎖懶漢式
由於線程不安全,我們可以通過synchronized
關鍵字進行處理。此時代碼如下
public class LazySynchronized {
private static LazySynchronized instance = null;
/**
* 私有化構造函數
*/
private LazySynchronized() {
}
public static synchronized LazySynchronized getInstance() {
if (instance == null) {
instance = new LazySynchronized();
}
return instance;
}
}
這種實現方式雖然線程安全,但是每次獲取實例都要加鎖,耗費資源,其實只要實例已經生成,以後獲取就不需要再鎖了。
基於這些缺點,我們可以在使用雙重檢查,對懶漢式進行進一步升級
3、雙重檢查
public class LazyDoubleCheck implements Serializable {
private static LazyDoubleCheck instance = null;
/**
* 私有化構造函數
*/
private LazyDoubleCheck() {
}
public static LazyDoubleCheck getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheck.class) {
if (instance == null) {
instance = new LazyDoubleCheck();
}
}
}
return instance;
}
}
這樣寫,只把新建實例的代碼放到同步鎖中,爲了保證線程安全再在同步鎖中加一個判斷,
雖然看起來更繁瑣,但是同步中的內容只會執行一次,執行過後,以後經過外層的if判斷後,都不會在執行了,
所以不會再有阻塞。程序運行的效率也會更加的高。
這種方式看似很好的解決了同步的問題,但是其中還是有個坑。當多個線程訪問這個方法時,
可能會返回還未完成初始化的對象!
問題就在於instance = new LazyDoubleCheck();
根據Java類的初始化過程,步驟instance = new LazyDoubleCheck();
並不是原子性的。
其中大概可以分爲三個步驟
-
- 分配內存給對象
-
- 初始化對象
-
- 設置instance指向剛分配的內存地址
但是在實際執行中代碼可能會被重排序,如下所示
-
- 分配內存給對象
-
- 設置instance指向剛分配的內存地址
-
- 初始化對象
在Java語言規範中,所有線程在執行Java程序時,必須要遵守intra-thread semantics規定。
intra-thread semantics保證重排序不會改變單線程內的程序執行結果。會允許那些在單線程內,不會改變單線程程序執行結果的重排序。例如上面的2和3步驟雖然被重排序了,但並不會影響程序執行結果。這個重排序在沒有改變單線程程序的執行結果的前提下,可以提高程序的執行性能。
爲了更好的理解這個問題,參考一下圖示
線程1執行到instance = new LazyDoubleCheck()
時發生重排序先執行第三步,此時instance被賦值但是還未初始化對象,這時線程2訪問到第一個if中,由於此時判斷instance不爲null就直接返回此對象,線程2獲得的就是還未初始化完全的對象。注意:由於重排序問題並不一定會發生
針對上面出現的問題,我們可以有兩個解決方案
- 1、不允許步驟2和3重排序
- 2、允許重排序,但是不允許其他線程"看到"重排序過程
針對方案一有以下實現,基於volatile
關鍵字禁止重排序
4、基於volatile
的雙重檢查
public class LazyDoubleCheck implements Serializable {
private static volatile LazyDoubleCheck instance = null;
/**
* 私有化構造函數
*/
private LazyDoubleCheck() {
}
public static LazyDoubleCheck getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheck.class) {
if (instance == null) {
instance = new LazyDoubleCheck();
}
}
}
return instance;
}
}
volatile
關鍵字具有內存可見性,關於這個以後會寫博客細談
針對方案二,我們通過靜態內部類的方式來實現。
5、靜態內部類
public class StaticInnerClass {
private static class InstanceHolder {
private static final StaticInnerClass instance = new StaticInnerClass();
}
/**
* 私有化構造函數
*/
private StaticInnerClass() {
}
public static StaticInnerClass getInstance() {
return InstanceHolder.instance;
}
}
這種實現方式是基於類的初始化鎖來實現類的懶加載安全性
利用了ClassLoader
的機制來保證初始化instance時只有一個線程,同時實現了延時加載
優點:既避免了同步帶來的性能損耗,又能夠延遲加載
關於雙重檢查鎖的原理還可以參考這篇博客https://blog.csdn.net/li295214001/article/details/48135939/
6、餓漢式
特點:在類加載時就完成了初始化,所以類加載比較慢,但獲取對象的速度快,同時無法做到延時加載
public class Hungry {
private static final Hungry hungry = new Hungry();
/**
* 構造函數私有化
*/
private Hungry() {
}
public static Hungry getInstance() {
return hungry;
}
}
好處:線程安全;獲取實例速度快 缺點:類加載即初始化實例,內存浪費。如果實例未使用,仍然會生成佔用內存
以上方式都各自實現了單例模式,但是並不能完全保障單例安全。當我們對一個對象進行序列化與反序列化後,上述單實例就無法保證
public static void main(String[] args) throws Exception {
Hungry s = Hungry.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
oos.writeObject(s);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Hungry s1 = (Hungry) ois.readObject();
ois.close();
System.out.println(s + "\n" + s1);
}
當我們執行上述序列化代碼時,程序中就會生成兩個單例對象,針對這種方式我們要如何保證單例呢
7、序列化安全的單例
此處我們對餓漢式單例進行改造,使其保證序列化時仍然是單例。
解決方案:在單例中增加readResolve
方法
public class Hungry implements Serializable{
private static final Hungry hungry = new Hungry();
/**
* 私有化構造函數
*/
private Hungry() {
}
public static Hungry getInstance() {
return hungry;
}
public Object readResolve(){
return hungry;
}
}
此時如果我們再測試序列化,就會發現返回的對象是同一個。這是爲什麼呢?爲什麼重寫readResolve()
方法就能實現序列化安全呢。關鍵還是在於ObjectInputStream
的readObject()
方法。查看源碼,我們就能一探究竟
以下JDK源碼基於JDK1.8.0_162
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
int outerHandle = passHandle;
try {
//返回的obj是readObject0()生成的
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
//中間省略部分代碼
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
查看方法可以看到返回的obj是readObject0()生成的,再進入readObject0()中
private Object readObject0(boolean unshared) throws IOException {
//省略部分代碼
try {
switch (tc) {
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
//可以看出object類型返回的對象都是以下返回的
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
//以下部分代碼省略
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
從以上部分代碼可以看出返回的代碼是checkResolve(readOrdinaryObject(unshared))
,首先查看readOrdinaryObject(unshared)
方法
private Object readOrdinaryObject(boolean unshared) throws IOException{
//省略部分代碼。。。
//這個obj就是返回的對象,只需要關注這個對象就行了
Object obj;
try {
//如果實現了serializable/externalizable接口isInstantiable()就會返回true
//此時基於反射生成了實例
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//中間省略部分代碼...
//進入if條件中,
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) {
//這裏通過反射會調用方法生成rep對象,在下面rep會賦值給obj對象
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);
}
}
//此時obj指向剛纔通過invokeReadResolve()方法生成的對象
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
在readOrdinaryObject
方法中待返回的對象obj
首先指向通過反射生成的實例
obj = desc.isInstantiable() ? desc.newInstance() : null;
(因爲實現了serializable/externalizable接口isInstantiable()
返回true)。
在下面的if條件中生成了rep對象,並且在下面將rep賦值給obj
handles.setObject(passHandle, obj = rep);
因此將重點放在Object rep = desc.invokeReadResolve(obj);
中,查看invokeReadResolve
方法
Object invokeReadResolve(Object obj)throws IOException, UnsupportedOperationException {
requireInitialized();
if (readResolveMethod != null) {
try {
//就是一個反射調用,關鍵是這個readResolveMethod是什麼
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
//省略部分代碼。。。
}
} else {
throw new UnsupportedOperationException();
}
}
在類中查找readResolveMethod
可以看到就是ObjectStreamClass
中定義的一個私有變量
private Method readResolveMethod;
繼續查找可以看到賦值readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
就是定義readResolve
的Method對象,通過反射調用readResolve
方法,將產生的對象在返回給obj
再返回。
此時返回到readObject0
方法中,readOrdinaryObject(unshared)
返回的值傳到checkResolve()
中
private Object checkResolve(Object obj) throws IOException {
if (!enableResolve || handles.lookupException(passHandle) != null) {
return obj;
}
//又調用了resolveObject方法,將返回的對象返回去
Object rep = resolveObject(obj);
if (rep != obj) {
// The type of the original object has been filtered but resolveObject
// may have replaced it; filter the replacement's type
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, rep);
}
return rep;
}
繼續查看resolveObject
方法定義
protected Object resolveObject(Object obj) throws IOException {
return obj;
}
在resolveObject
中直接將obj返回了。此時所有流程基本走完。
到這裏我們就可以知道爲什麼在類中定義readResolve
方法返回單例對象就可以防止序列化破壞單例了。
但是注意在上面代碼中有一步obj = desc.isInstantiable() ? desc.newInstance() : null;
雖然最後返回的是單例對象,但其實在執行過程還是通過反射生成了新的對象,雖然最後返回的並不是這個對象。
那麼如何防止反射生成多個對象呢?
8、防止反射的單例
對於餓漢模式來說,運行以下代碼通過反射創建對象仍然會產生多個實例
public static void main(String[] args) throws Exception {
Class<Hungry> objClass = Hungry.class;
Constructor<Hungry> constructor = objClass.getDeclaredConstructor();
constructor.setAccessible(true);
Hungry instance = Hungry.getInstance();
Hungry newInstance = constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(newInstance == instance);
}
執行結果
Hungry@6d6f6e28
Hungry@135fbaa4
false
此時就需要對私有構造器改造一下,禁止其反射調用
private Hungry() {
if (hungry != null) {
throw new RuntimeException("單例構造器禁止反射!");
}
}
此時在執行上述代碼就會報錯
Exception in thread "main" java.lang.reflect.InvocationTargetException
at ReflectTest.main(ReflectTest.java:24)
Caused by: java.lang.RuntimeException: 單例構造器禁止反射!
at Hungry.<init>(Hungry.java:25)
... 5 more
對於靜態內部類實現的單例模式也是這種方式防止反射
private StaticInnerClass() {
if (InstanceHolder.instance != null) {
throw new RuntimeException("單例構造器禁止反射!");
}
}
注意,對於這種調用getInstance
方法時就已經完成類初始化的單例(餓漢模式和靜態內部類),這種方式可以防止反射。但是對於類似懶漢模式這種,調用getInstance
纔會初始化的單例,可能會有一些問題。
注意在上面反射創建對象的代碼中,是先調用的getInstance
方法,然後才使用反射創建了對象,這樣是沒有問題的。但如果兩者調換的位置,先通過反射創建對象,然後再調用getInstance
方法。即便我們再構造器中添加代碼仍然會出現問題
private LazyInstance() {
if (instance != null) {
throw new RuntimeException("單例構造器禁止反射!");
}
}
public static void main(String[] args) throws Exception {
Class<LazyInstance> objClass = LazyInstance.class;
Constructor<LazyInstance> constructor = objClass.getDeclaredConstructor();
constructor.setAccessible(true);
LazyInstance newInstance = constructor.newInstance();
LazyInstance instance = LazyInstance.getInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(newInstance == instance);
}
運行結果
LazyInstance@6d6f6e28
LazyInstance@135fbaa4
false
當使用反射創建對象時,由於還沒調用getInstance
方法,此時instance還爲null,就不會拋出異常。
當然我們可以設置一個變量flag標記是否初始化,然後再構造器中通過該變量進行判斷,但是既然構造器可以通過反射調用,設置變量仍然是可以被反射修改的
有沒有完美的單例模式嗎
9. 枚舉類型單例
這是單例模式的完美實現
public enum EnumSingleton {
/**
* 單實例
*/
INSTANCE
public void doSomething() {
System.out.println("you can do something");
}
public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
}
}
這種方式實現了線程安全,序列化安全,自帶防止反射
後記:理論上原型模式也會破壞單例,但是基本上沒人會在單例上運用原型模式,在下一篇原型模式之後再補充