在Java中慌随,單例模式分為很多種团甲,本人所了解的單例模式有以下幾種溉贿,如有不全還請大家留言指點:
- 餓漢式
- 懶漢式/Double check(雙重檢索)
- 靜態(tài)內(nèi)部類
- 枚舉單例
一仑嗅、餓漢式
餓漢式是在jvm加載這個單例類的時候畦浓,就會初始化這個類中的實例痹束,在使用單例中的實例時直接拿來使用就好,因為加載這個類的時候就已經(jīng)完成初始化讶请,并且由于是已經(jīng)加載好的單例實例因此是線程安全的祷嘶,并發(fā)獲取的情況下不會有問題,是一種可投入使用的可靠單例夺溢。
優(yōu)點:使用起來效率高论巍、線程安全
缺點:由于jvm在加載單例類的時候需要初始化單例實例,因此在加載單例的時候針對jvm內(nèi)存不夠友好风响。
二嘉汰、懶漢式
最簡單的懶漢式,核心思想就是彌補餓漢式的缺點状勤,在jvm加載單例類的時候不去初始化實例鞋怀,而是在第一次獲取實例的時候再去初始化實例。但是這樣理論完美的單例在使用的時候有一個致命的缺點持搜,在多線程使用的情況下密似,有時會出現(xiàn)不同線程從單例實例中獲取不同的實體。針對多線程環(huán)境中并不可靠葫盼。
優(yōu)點:針對jvm內(nèi)存比較友好辛友,實現(xiàn)了實例的懶加載。
缺點:多線程環(huán)境下不安全剪返,會出現(xiàn)不同線程從單例實例中獲取不同的實體的情況废累。
具體為什么會出現(xiàn)不同線程從單例實例中獲取不同的實體的情況呢?如下圖脱盲,我們通過分析去解釋邑滨,為何他是線程不安全的。
假設(shè)钱反,當前有兩個線程同時首次獲取此單例中的實例時:
- 線程一執(zhí)行g(shù)etInstence方法掖看,并判斷instance實例是否已經(jīng)被初始化匣距。
- 線程一判斷instance為null,執(zhí)行到 2 處哎壳,此時線程一還沒有開始執(zhí)行毅待,然后執(zhí)行權(quán)被線程二獲取,線程一進入等待归榕。
- 線程二執(zhí)行到 1 處判斷instance為null尸红,因為線程一即將開始初始化instance,但是還沒有初始化刹泄。
- 線程二執(zhí)行到 2 處開始初始化instence方法外里,并完成初始化,返回一個instance實例特石。
- 這時線程一被喚醒盅蝗,繼續(xù)從 2 處執(zhí)行,開始初始化instence方法姆蘸,并且也返回一個instance實例墩莫。
這樣,線程一和線程二從單例中獲取了兩個不同的實例逞敷。針對懶漢式的這種線程不安全的現(xiàn)象狂秦,攻城獅們也是開始頭腦風暴來改善它,比較容易想到的是將getInstence方法加鎖兰粉,來實現(xiàn)懶漢式的線程安全:
這樣雖然看似解決問題了故痊,但是未免太過于激進了,synchronized鎖住獲取實例的整個方法玖姑,因此在并發(fā)獲取單例實例的時候會有性能問題愕秫,并且線程安全問題的出現(xiàn)只是在第一次獲取實例的情況才會出現(xiàn),初始化之后不會再出現(xiàn)性能問題焰络,synchronized鎖的運用未免因小失大戴甩。
于是為了線程安全,還為了能在并發(fā)情況下高效的性能闪彼,便有了Double check(雙重檢索)的懶漢式單例
Double check的理論為:當?shù)谝淮蝿?chuàng)建單例實例的時候甜孤,只有一個線程可以去創(chuàng)建實例,因此不會出現(xiàn)多個線程獲取不同實例的情況畏腕。
假設(shè)時間序列:
- 線程一進入getInstence方法
- 線程一判斷instence為null缴川,并在 1 處進入synchronize塊,此時線程二開始執(zhí)行描馅,線程一等待
- 線程二進入getInstence方法把夸,判斷instence為null,并準備進入synchronize塊铭污,此時發(fā)現(xiàn)synchronize塊的鎖被占用恋日,因此進入等待
- 線程一開始再次判斷instence為null膀篮,然后開始初始化instence實例,然后釋放synchronize的鎖岂膳,獲取到了實例執(zhí)行完成
- 此時線程二開始得到synchronize鎖誓竿,進入synchronize塊再次判斷instence是否為null,發(fā)現(xiàn)instence此時已經(jīng)有值谈截,釋放鎖筷屡,直接獲取instence實例返回
Double check的理論看起來非常的完美,然而一切到頭來發(fā)現(xiàn)僅僅是想得美而已傻盟,在實際運行中他還是有問題的速蕊。
年輕稚嫩的猿也許會一臉懵逼嫂丙,老謀深算的猿也許會微微一笑娘赴,但是可能他們都會想 弄啥子嘞?
其實跟啤,這個理論的失敗诽表,并不是jvm實現(xiàn)的bug,而是歸咎于Java平臺的內(nèi)存模型隅肥,Java的內(nèi)存模型是圍繞著如何在并發(fā)過程中處理原子性竿奏、可見性、有序性這3個特征建立的腥放,而針對有序性泛啸,引用深入JVM虛擬機中的一句話解釋是:如果在本線程中觀察,所有的操作都是有序的秃症;如果在一個線程中觀察另一個線程候址,所有的操作都是無序的。前半句是指“線程內(nèi)表現(xiàn)為串行指令”种柑,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象岗仑。而針對原子性,看似簡單一行代碼聚请,經(jīng)過虛擬機編譯成字節(jié)碼信息后荠雕,可能就不是一行代碼了。而針對可見性驶赏,一個線程改變的變量值炸卑,并不會立刻對其他線程可見。
而上面Double check代碼失敗的源頭就是 instence = new DoubleCheck(); 這句話煤傍,而這句看似簡的一句話盖文,其實在虛擬機中分成了三個步驟:
- 為即將實例化的對象分配內(nèi)存空間
- 初始化單例實體對象執(zhí)行構(gòu)造函數(shù)
- 將內(nèi)存空間地址賦值給instence實例引用
也就是說其實我們所謂的new對象 并不是一個原子操作,并且患久,針對上面的2 3 步驟虛擬機會進行指令重排序椅寺,如果上面的Double check代碼的對象實例化的經(jīng)過重排序順序變成1 3 2 的話浑槽,就會出現(xiàn)問題:
- 線程一進入getInstence方法
- 線程一判斷instence為null,并在 1 處進入synchronize塊
- 線程一再次判斷instence為null返帕,最后執(zhí)行到 3 處桐玻,然而分配完內(nèi)存,獲取到實例地址荆萤,此時instence不再為null镊靴,但是還未初始化對象執(zhí)行構(gòu)造方法,此時縣城而獲取執(zhí)行權(quán)链韭,線程一被掛起
- 線程二獲取getInstence方法偏竟,并判斷instence不再null,然后獲取到了一個instence對象的地址敞峭,但是此時instence對象并未完成初始化踊谋,線程二后續(xù)執(zhí)行就會出現(xiàn)問題
- 線程一此時蘇醒,完成后面的instence對象初始化的動作旋讹,并返回實例
然而在jdk1.5以后殖蚕,這種情況有了解決方法,原因在于jdk1.5開始針對volatile進行了增強沉迹,volatile變量開始可以屏蔽指令重排睦疫,也就是說
當我們將instence引用進行volatile進行修飾的話instence = new DoubleCheck();這句話中的指令將不會被指令重排序,Double check也就不再只是想想了鞭呕。附上完整代碼:
三蛤育、靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類的優(yōu)點是:外部類加載時并不會立即加載內(nèi)部類,內(nèi)部類不被加載就不去初始化實例葫松,因此實現(xiàn)了懶加載瓦糕。當StaticSingle第一次被加載時,并不需要去加載內(nèi)部類Holder进宝,只有當getInstance()方法第一次被調(diào)用時刻坊,才會導(dǎo)致虛擬機加載Holer類菜會去初始化StaticSingle實例。這種方法不僅能確保線程安全党晋,也能保證單例的唯一性谭胚,同時也延遲了單例的實例化。
那么靜態(tài)內(nèi)部類是如何實現(xiàn)線程安全的呢未玻?我們需要了解下面一些只是
針對于類的初始化灾而,JVM虛擬機嚴格規(guī)定了有且僅有5種情況必須對類進行“初始化“:
- 遇到new、getstatic扳剿、setstatic或者invikestatic這4個字節(jié)碼指令時旁趟,對應(yīng)的java代碼場景為:new一個關(guān)鍵字或者一個實例化對象時、讀取或設(shè)置一個靜態(tài)字段時(final修飾庇绽、已在編譯期把結(jié)果放入常量池的除外)锡搜、調(diào)用一個類的靜態(tài)方法時橙困。
- 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒進行初始化耕餐,需要先調(diào)用其初始化方法進行初始化凡傅。
- 當初始化一個類時,如果其父類還未進行初始化肠缔,會先觸發(fā)其父類的初始化夏跷。
- 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的類)明未,虛擬機會先初始化這個類槽华。
- 當使用JDK 1.7等動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic趟妥、REF_putStatic猫态、REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進行過初始化煮纵,則需要先觸發(fā)其初始化懂鸵。
這5種情況被稱為是類的主動引用偏螺,注意行疏,這里《虛擬機規(guī)范》中使用的限定詞是"有且僅有",那么套像,除此之外的所有引用類都不會對類進行初始化酿联,稱為被動引用。靜態(tài)內(nèi)部類就屬于被動引用的行列夺巩。
我們再回頭看下getInstance()方法贞让,調(diào)用的是Holer.INSTANCE,取的是Holer里的INSTANCE對象柳譬,跟上面那個DCL方法不同的是喳张,getInstance()方法并沒有多次去new對象盯漂,故不管多少個線程去調(diào)用getInstance()方法棠笑,取的都是同一個INSTANCE對象,而不用去重新創(chuàng)建奥帘。當getInstance()方法被調(diào)用時制跟,Holer才在StaticSingle的運行時常量池里舅桩,把符號引用替換為直接引用,這時靜態(tài)對象INSTANCE也真正被創(chuàng)建雨膨,然后再被getInstance()方法返回出去擂涛,這點同餓漢模式。那么INSTANCE在創(chuàng)建過程中又是如何保證線程安全的呢聊记?在《深入理解JAVA虛擬機》中撒妈,有這么一句話:
虛擬機會保證一個類的()方法在多線程環(huán)境中被正確地加鎖恢暖、同步,如果多個線程同時去初始化一個類狰右,那么只會有一個線程去執(zhí)行這個類的()方法胀茵,其他線程都需要阻塞等待,直到活動線程執(zhí)行()方法完畢挟阻。如果在一個類的()方法中有耗時很長的操作琼娘,就可能造成多個進程阻塞(需要注意的是,其他線程雖然會被阻塞附鸽,但如果執(zhí)行()方法后脱拼,其他線程喚醒之后不會再次進入()方法。同一個加載器下坷备,一個類型只會初始化一次熄浓。),在實際應(yīng)用中省撑,這種阻塞往往是很隱蔽的赌蔑。
故而,可以看出INSTANCE在創(chuàng)建過程中是線程安全的竟秫,所以說靜態(tài)內(nèi)部類形式的單例可保證線程安全娃惯,也能保證單例的唯一性,同時也延遲了單例的實例化肥败。
那么趾浅,是不是可以說靜態(tài)內(nèi)部類單例就是最完美的單例模式了呢?其實不然馒稍,靜態(tài)內(nèi)部類也有著一個致命的缺點皿哨,就是傳參的問題,由于是靜態(tài)內(nèi)部類的形式去創(chuàng)建單例的纽谒,故外部無法傳遞參數(shù)進去证膨,例如Context這種參數(shù),所以鼓黔,我們創(chuàng)建單例時央勒,可以在靜態(tài)內(nèi)部類與DCL模式里自己斟酌。
四请祖、枚舉單例
從上述3種單例模式的寫法中订歪,似乎也解決了效率或者懶加載以及線程安全的問題,但是它們都有兩個共同的缺點:
- 序列化可能會破壞單例模式肆捕,比較每次反序列化一個序列化的對象實例時都會創(chuàng)建一個新的實例刷晋,解決方案如下:
- 使用反射強行調(diào)用私有構(gòu)造器,解決方式可以修改構(gòu)造器,讓它在創(chuàng)建第二個實例的時候拋異常眼虱,解決方案如下:
如上所述喻奥,問題確實也得到了解決,但問題是我們?yōu)榇烁冻隽瞬簧倥δ笮刺砑恿瞬簧俅a撞蚕,還應(yīng)該注意到如果單例類維持了其他對象的狀態(tài)時還需要使他們成為transient的對象,這種就更復(fù)雜了过牙,那有沒有更簡單更高效的呢甥厦?當然是有的,那就是枚舉單例了寇钉,先來看看如何實現(xiàn):
代碼相當簡潔刀疙,我們也可以像常規(guī)類一樣編寫enum類,為其添加變量和方法扫倡,訪問方式也更簡單谦秧,使用EnumSingle.INSTANCE進行訪問,這樣也就避免調(diào)用getInstance方法撵溃,更重要的是使用枚舉單例的寫法疚鲤,我們完全不用考慮序列化和反射的問題。枚舉序列化是由jvm保證的缘挑,每一個枚舉類型和定義的枚舉變量在JVM中都是唯一的集歇。
在枚舉類型的序列化和反序列化上,Java做了特殊的規(guī)定:在序列化時Java僅僅是將枚舉對象的name屬性輸出到結(jié)果中卖哎,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據(jù)名字查找枚舉對象鬼悠。同時,編譯器是不允許任何對這種序列化機制的定制的并禁用了writeObject亏娜、readObject、readObjectNoData蹬挺、writeReplace和readResolve等方法维贺,從而保證了枚舉實例的唯一性,這里我們不妨再次看看Enum類的valueOf方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
實際上通過調(diào)用enumType(Class對象的引用)的enumConstantDirectory方法獲取到的是一個Map集合巴帮,在該集合中存放了以枚舉name為key和以枚舉實例變量為value的Key&Value數(shù)據(jù)溯泣,因此通過name的值就可以獲取到枚舉實例,看看enumConstantDirectory方法源碼:
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
//getEnumConstantsShared最終通過反射調(diào)用枚舉類的values方法
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
//map存放了當前enum類的所有枚舉實例變量榕茧,以name為key值
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;
到這里我們也就可以看出枚舉序列化確實不會重新創(chuàng)建新實例垃沦,jvm保證了每個枚舉實例變量的唯一性。再來看看反射到底能不能創(chuàng)建枚舉用押,下面試圖通過反射獲取構(gòu)造器并創(chuàng)建枚舉
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
//獲取枚舉類的構(gòu)造函數(shù)(前面的源碼已分析過)
Constructor<EnumSingle> constructor=EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
//創(chuàng)建枚舉
EnumSingle singleton=constructor.newInstance("otherInstance",9);
}
執(zhí)行報錯
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at zejian.SingletonEnum.main(SingletonEnum.java:38)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
顯然告訴我們不能使用反射創(chuàng)建枚舉類肢簿,這是為什么呢?不妨看看newInstance方法源碼:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//這里判斷Modifier.ENUM是不是枚舉修飾符,如果是就拋異常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
源碼很了然池充,確實無法使用反射創(chuàng)建枚舉實例桩引,也就是說明了創(chuàng)建枚舉實例只有編譯器能夠做到而已。顯然枚舉單例模式確實是很不錯的選擇收夸,因此我們推薦使用它坑匠。但是這總不是萬能的,對于android平臺這個可能未必是最好的選擇卧惜,在android開發(fā)中厘灼,內(nèi)存優(yōu)化是個大塊頭,而使用枚舉時占用的內(nèi)存常常是靜態(tài)變量的兩倍還多咽瓷,因此android官方在內(nèi)存優(yōu)化方面給出的建議是盡量避免在android中使用enum手幢。但是不管如何,關(guān)于單例忱详,我們總是應(yīng)該記孜Ю础:線程安全,延遲加載匈睁,序列化與反序列化安全监透,反射安全是很重重要的。
至此航唆,單例模式的介紹完畢胀蛮,不足之處大家補充指點。
參考:
https://blog.csdn.net/chenchaofuck1/article/details/51702129
https://blog.csdn.net/mnb65482/article/details/80458571
《深入理解Java虛擬機 JVM高級特性與最佳實踐》