Java枚舉單例模式比DCL和靜態(tài)單例要好?
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進!你不來,我和你的競爭對手一起精進!
編輯:業(yè)余草
liuchenyang0515.blog.csdn.net
推薦:https://www.xttblog.com/?p=5340
?餓漢式懶漢式單例就不說了,DCL和靜態(tài)單例簡單介紹下,為后面講解枚舉單例作鋪墊。分析不易,歡迎一鍵三連~
?
文章目錄
雙重校驗鎖單例 為什么要double check?去掉第二次check行不行 singleton為什么要加上volatile關鍵字 靜態(tài)內(nèi)部類單例 枚舉單例 (推薦??!) 枚舉單例模式的使用 反編譯分析單例枚舉類
1. 雙重校驗鎖單例(DCL)
public?class?Singleton?{
????private?static?volatile?Singleton?singleton;
????private?Singleton(){
????}
????
????public?static?Singleton?getInstance(){
????????if?(singleton?==?null){
????????????synchronized?(Singleton.class){
????????????????if?(singleton?==?null){
????????????????????singleton?=?new?Singleton();
????????????????}
????????????}
????????}
????????return?singleton;
????}
}
這種DCL寫法的優(yōu)點:不僅線程安全,而且延遲加載。
1.1 為什么要double check?去掉第二次check行不行?
當然不行,當2個線程同時執(zhí)行getInstance方法時,都會執(zhí)行第一個if判斷,由于鎖機制的存在,會有一個線程先進入同步語句,而另一個線程等待,當?shù)谝粋€線程執(zhí)行了new Singleton()之后,就會退出synchronized的保護區(qū)域,這時如果沒有第二重if判斷,那么第二個線程也會創(chuàng)建一個實例,這就破壞了單例。
1.2 singleton為什么要加上volatile關鍵字?
主要原因就是 singleton = new Singleton();不是一個原子操作。
在JVM中,這句語句至少做了3件事
1.給 Singleton的實例分配內(nèi)存空間;2.調(diào)用 Singleton()的構造函數(shù),初始化成員字段;3.將 singleton指向分配的內(nèi)存空間(此時singleton就不是null了)
因為存在著指令重排序的優(yōu)化,第2、3步的順序是不能保證的,最后的執(zhí)行順序可能是1-2-3,也可能是1-3-2,假如執(zhí)行順序是1-3-2,我們看看會出現(xiàn)什么問題。

雖然singleton不是null,但是指向的空間并沒有初始化,還是會報錯,這就是DCL失效的問題,這種問題難以跟蹤難以重現(xiàn)可能會隱藏很久。
JDK1.5之前JMM(Java Memory Model,即Java內(nèi)存模型)中的Cache、寄存器到主存的回寫規(guī)定,上面第二第三的順序無法保證。JDK1.5之后,SUN官方調(diào)整了JVM,具體化了volatile關鍵字,private static volatile Singleton singleton;只要加上volatile,就可以保證每次從主存中讀?。ㄟ@涉及到CPU緩存一致性問題,不在本文探討范圍內(nèi),有興趣自行搜索),也可以防止指令重排序的發(fā)生,避免拿到未完成初始化的對象。
簡單講,**volatile主要就是限制JIT編譯器優(yōu)化**,編譯器優(yōu)化常用的方法有:
將內(nèi)存變量緩存到寄存器; 調(diào)整指令順序充分利用 CPU指令流水線,常見的是重新排序讀寫指令。
如果沒有volatile關鍵字,則編譯器可能優(yōu)化讀取,使用寄存器中的緩存值,如果這個變量由別的線程更新了的話,將出現(xiàn)實際值和讀取的值不一致。「使用了volatile后,編譯器讀取的時候跳過緩存,直接在內(nèi)存中的實際位置讀變量,寫的時候通知其他緩存更新,這就是所謂的保證內(nèi)存可見性,并且使用volatile還能禁止指令重排序?!?/strong>
public?volatile?int?a?=?11;
......
int?c?=?6;
c?=?a;//?執(zhí)行這一句的時候,在高并發(fā)情況下,a如果被修改為22,那么c會被賦值為22而不是11
//如果a不被volatile修飾,c有小概率被賦值為11,因為c取寄存器的緩存副本11還沒來得及更新
2. 靜態(tài)內(nèi)部類單例
public?class?Singleton{
?private?Singleton(){}
?
?private?static?class?SingletonInstance?{
?????private?static?Singleton?singleton?=?new?Singleton();
?}
?
?public?static?Singleton?getInstance(){
???return?SingletonInstance.singleton;
?}
}
與餓漢式的區(qū)別就在于,類加載的時候,這里并不會實例化對象,只有調(diào)用getInstance方法才會實例化對象。
和DCL優(yōu)點一樣,延遲加載,效率高。
雖然DCL和靜態(tài)單例都不錯,但是它們并不能防止反序列化和反射生成多個實例。更好的寫法當然是枚舉單例了!
3. 枚舉單例 (推薦??!)
其他所有的實現(xiàn)單例的方式其實是有問題的,那就是可能被反序列化和反射破壞。
我們來看看JDK1.5中添加的枚舉類來實現(xiàn)單例
public?enum?Singleton?{
?INSTANCE,
?public?void?testMethod()?{
??
?}
}
枚舉的寫法的優(yōu)點:
1.不用考慮懶加載和線程安全的問題,代碼寫法簡潔優(yōu)雅
2.線程安全
反編譯任何一個枚舉類會發(fā)現(xiàn),枚舉類里的各個枚舉項是是通過static代碼塊來定義和初始化的(可以見后面3.2節(jié)反編譯分析單例枚舉有分析到這個),它們會在類被加載時完成初始化,而java類的加載由JVM保證線程安全,所以,創(chuàng)建一個Enum類型的枚舉是線程安全的
防止破壞單例
我們知道,序列化可以將一個單例的實例對象寫到磁盤,然后再反序列化讀回來,從而獲得一個新的實例。即使構造函數(shù)是私有的,反序列化時依然可以通過特殊的途徑去創(chuàng)建類的一個新的實例,相當于調(diào)用該類的構造函數(shù)。
Java對枚舉的序列化作了規(guī)定,在序列化時,僅將枚舉對象的name屬性輸出到結果中,在反序列化時,就是通過java.lang.Enum的valueOf來根據(jù)名字查找對象,而不是新建一個新的對象。「枚舉在序列化和反序列化時,并不會調(diào)用構造方法」,這就防止了反序列化導致的單例破壞的問題。
對于反射破壞單例的而言,枚舉類有同樣的防御措施,反射在通過newInstance創(chuàng)建對象時,會檢查這個類是否是枚舉類,如果是,會拋出異常java.lang.IllegalArgumentException: Cannot reflectively create enum objects,表示反射創(chuàng)建對象失敗。
綜上,枚舉可以防止反序列化和反射破壞單例。
3.1 枚舉單例模式的使用
//?Singleton.java
public?enum?Singleton?{
????INSTANCE;
????public?void?testMethod()?{
????????System.out.println("執(zhí)行了單例類的方法");
????}
}
//?Test.java
public?class?Test?{
?public?static?void?main(String[]?args)?{
????????//演示如何使用枚舉寫法的單例類
????????Singleton.INSTANCE.testMethod();
????????System.out.println(Singleton.INSTANCE);
????}
}
運行結果如下:

3.2 反編譯分析單例枚舉類
為了讓大家進一步了解枚舉類,我們將上面枚舉單例類進行反編譯javap -p Singleton.class,其中-p的意思是反編譯的時候要包含私有方法。
//?這是反編譯后的內(nèi)容
public?final?class?Singleton?extends?java.lang.Enum<Singleton>?{
??public?static?final?Singleton?INSTANCE;
??private?static?final?Singleton[]?$VALUES;
??public?static?Singleton[]?values();
??public?static?Singleton?valueOf(java.lang.String);
??private?Singleton();
??public?void?testMethod();
??static?{};
}
我們可以看到,
INSTANCE是Singleton類的實例Singleton繼承了java.lang.Enum類這里還有一個私有的 Singleton的無參構造方法,枚舉類的枚舉項都會使用這個構造方法來實例化,也就是說,這里的INSTANCE會使用這個構造方法來實例化。實例化的過程發(fā)生在最后空的 static代碼塊中,可以通過javap的其他參數(shù)進一步分析static里面的字節(jié)碼內(nèi)容,static里面其實包含了很多字節(jié)碼指令,這些指令在做枚舉項INSTANCE的初始化工作,而static代碼塊是在類加載的時候執(zhí)行的,也就是說Singleton類被加載的時候,INSTANCE就被初始化了。static代碼塊里面除了初始化INSTANCE,Singleton[] $VALUES這個定義的私有的數(shù)組也是在static里面創(chuàng)建和初始化的。然后把所有枚舉項按照定義的順序放入這個$VALUES數(shù)組中,最后我們可以通過values方法來訪問這個數(shù)組
為了分析每個方法中的操作,我們使用javap -p -c -v Singleton.class來看看更詳細的,-c來看每個方法中的字節(jié)碼,-v把常量池信息也打印出來,這里了解即可,看不懂就看我上面的結論吧,重點只需要看static代碼塊部分的字節(jié)碼,下面是為結論做一個驗證。
/**
?*?@author:?磚業(yè)洋__
?*?@description:?我重點只分析最后的static部分
?*/
public?final?class?Singleton?extends?java.lang.Enum<Singleton>
??minor?version:?0
??major?version:?52
??flags:?ACC_PUBLIC,?ACC_FINAL,?ACC_SUPER,?ACC_ENUM
Constant?pool:?????//?需要注意常量池的部分,后面分析每條指令的時候可以回到這里查閱
???#1?=?Fieldref???????????#4.#37?????????//?Singleton.$VALUES:[LSingleton;
???#2?=?Methodref??????????#38.#39????????//?"[LSingleton;".clone:()Ljava/lang/Object;
???#3?=?Class??????????????#17????????????//?"[LSingleton;"
???#4?=?Class??????????????#40????????????//?Singleton
???#5?=?Methodref??????????#13.#41????????//?java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
???#6?=?Methodref??????????#13.#42????????//?java/lang/Enum."":(Ljava/lang/String;I)V
???#7?=?Fieldref???????????#43.#44????????//?java/lang/System.out:Ljava/io/PrintStream;
???#8?=?String?????????????#45????????????//?執(zhí)行了單例類的方法
???#9?=?Methodref??????????#46.#47????????//?java/io/PrintStream.println:(Ljava/lang/String;)V
??#10?=?String?????????????#14????????????//?INSTANCE
??#11?=?Methodref??????????#4.#42?????????//?Singleton."":(Ljava/lang/String;I)V
??#12?=?Fieldref???????????#4.#48?????????//?Singleton.INSTANCE:LSingleton;
??#13?=?Class??????????????#49????????????//?java/lang/Enum
??#14?=?Utf8???????????????INSTANCE
??#15?=?Utf8???????????????LSingleton;
??#16?=?Utf8???????????????$VALUES
??#17?=?Utf8???????????????[LSingleton;
??#18?=?Utf8???????????????values
??#19?=?Utf8???????????????()[LSingleton;
??#20?=?Utf8???????????????Code
??#21?=?Utf8???????????????LineNumberTable
??#22?=?Utf8???????????????valueOf
??#23?=?Utf8???????????????(Ljava/lang/String;)LSingleton;
??#24?=?Utf8???????????????LocalVariableTable
??#25?=?Utf8???????????????name
??#26?=?Utf8???????????????Ljava/lang/String;
??#27?=?Utf8???????????????
??#28?=?Utf8???????????????(Ljava/lang/String;I)V
??#29?=?Utf8???????????????this
??#30?=?Utf8???????????????Signature
??#31?=?Utf8???????????????()V
??#32?=?Utf8???????????????testMethod
??#33?=?Utf8???????????????
??#34?=?Utf8???????????????Ljava/lang/Enum;
??#35?=?Utf8???????????????SourceFile
??#36?=?Utf8???????????????Singleton.java
??#37?=?NameAndType????????#16:#17????????//?$VALUES:[LSingleton;
??#38?=?Class??????????????#17????????????//?"[LSingleton;"
??#39?=?NameAndType????????#50:#51????????//?clone:()Ljava/lang/Object;
??#40?=?Utf8???????????????Singleton
??#41?=?NameAndType????????#22:#52????????//?valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
??#42?=?NameAndType????????#27:#28????????//?"":(Ljava/lang/String;I)V
??#43?=?Class??????????????#53????????????//?java/lang/System
??#44?=?NameAndType????????#54:#55????????//?out:Ljava/io/PrintStream;
??#45?=?Utf8???????????????執(zhí)行了單例類的方法
??#46?=?Class??????????????#56????????????//?java/io/PrintStream
??#47?=?NameAndType????????#57:#58????????//?println:(Ljava/lang/String;)V
??#48?=?NameAndType????????#14:#15????????//?INSTANCE:LSingleton;
??#49?=?Utf8???????????????java/lang/Enum
??#50?=?Utf8???????????????clone
??#51?=?Utf8???????????????()Ljava/lang/Object;
??#52?=?Utf8???????????????(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
??#53?=?Utf8???????????????java/lang/System
??#54?=?Utf8???????????????out
??#55?=?Utf8???????????????Ljava/io/PrintStream;
??#56?=?Utf8???????????????java/io/PrintStream
??#57?=?Utf8???????????????println
??#58?=?Utf8???????????????(Ljava/lang/String;)V
{
??public?static?final?Singleton?INSTANCE;?//?定義枚舉項
????descriptor:?LSingleton;
????flags:?ACC_PUBLIC,?ACC_STATIC,?ACC_FINAL,?ACC_ENUM
??private?static?final?Singleton[]?$VALUES;?//?定義對象數(shù)組,并沒有初始化,只是空引用
????descriptor:?[LSingleton;
????flags:?ACC_PRIVATE,?ACC_STATIC,?ACC_FINAL,?ACC_SYNTHETIC
??public?static?Singleton[]?values();
????descriptor:?()[LSingleton;
????flags:?ACC_PUBLIC,?ACC_STATIC
????Code:
??????stack=1,?locals=0,?args_size=0
?????????0:?getstatic?????#1??????????????????//?Field?$VALUES:[LSingleton;
?????????3:?invokevirtual?#2??????????????????//?Method?"[LSingleton;".clone:()Ljava/lang/Object;
?????????6:?checkcast?????#3??????????????????//?class?"[LSingleton;"
?????????9:?areturn
??????LineNumberTable:
????????line?1:?0
??public?static?Singleton?valueOf(java.lang.String);
????descriptor:?(Ljava/lang/String;)LSingleton;
????flags:?ACC_PUBLIC,?ACC_STATIC
????Code:
??????stack=2,?locals=1,?args_size=1
?????????0:?ldc???????????#4??????????????????//?class?Singleton
?????????2:?aload_0
?????????3:?invokestatic??#5??????????????????//?Method?java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
?????????6:?checkcast?????#4??????????????????//?class?Singleton
?????????9:?areturn
??????LineNumberTable:
????????line?1:?0
??????LocalVariableTable:
????????Start??Length??Slot??Name???Signature
????????????0??????10?????0??name???Ljava/lang/String;
??private?Singleton();
????descriptor:?(Ljava/lang/String;I)V
????flags:?ACC_PRIVATE
????Code:
??????stack=3,?locals=3,?args_size=3
?????????0:?aload_0???????//?棧操作指令,把局部方法表里的第0個位置的變量load加載到棧上來,a前綴表示它是一個引用類型。
???????//?提醒:?當JVM執(zhí)行一段代碼的時候,首先會把用到的所有的變量存在一個本地變量表里————局部變量表。
???????//?在棧上做計算的時候,需要使用局部方法表的值,就會通過load指令把它們加載到棧上來
???????//?在棧上運算完之后,需要把值存回到局部方法表,所以也會有對應的store指令,load和store是對應的。
?????????1:?aload_1
?????????2:?iload_2
?????????3:?invokespecial?#6??????????????????//?Method?java/lang/Enum."":(Ljava/lang/String;I)V
?????????6:?return
??????LineNumberTable:
????????line?1:?0
??????LocalVariableTable:
????????Start??Length??Slot??Name???Signature
????????????0???????7?????0??this???LSingleton;
????Signature:?#31??????????????????????????//?()V
??public?void?testMethod();
????descriptor:?()V
????flags:?ACC_PUBLIC
????Code:
??????stack=2,?locals=1,?args_size=1
?????????0:?getstatic?????#7??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream;
?????????3:?ldc???????????#8??????????????????//?String?執(zhí)行了單例類的方法
?????????5:?invokevirtual?#9??????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V
?????????8:?return
??????LineNumberTable:
????????line?5:?0
????????line?6:?8
??????LocalVariableTable:
????????Start??Length??Slot??Name???Signature
????????????0???????9?????0??this???LSingleton;
??static?{};
????descriptor:?()V.????????//?就是代表返回void類型
????flags:?ACC_STATIC
????Code:
??????stack=4,?locals=0,?args_size=0
?????????0:?new???????????#4??????????????????//?class?Singleton
?????????//?new?#4表示從常量池里拿到標號4這個類型的名字,往上看Constant?pool部分的定義可知就是Singleton這個類,然后new出來變成對象
?????????3:?dup??????????//?然后dup壓棧
?????????4:?ldc???????????#10?????????????????//?String?INSTANCE,將常量池中標號10的String類型的值INSTANCE推送到棧頂
?????????6:?iconst_0????????//?定義一個int類型的變量值為0,我也不知道這里定義個常量有什么卵用
?????????7:?invokespecial?#11?????????????????//?Method?"":(Ljava/lang/String;I)V,調(diào)用構造器初始化,返回類型為void
????????10:?putstatic?????#12?????????????????//?Field?INSTANCE:LSingleton;給INSTANCE這個靜態(tài)變量賦值,和name一樣
????????13:?iconst_1????????//?定義一個int類型的變量值為1,然并卵
????????14:?anewarray?????#4??????????????????//?class?Singleton,實例化一個裝Singleton枚舉類型的數(shù)組,這里就是$VALUES數(shù)組
????????17:?dup
????????18:?iconst_0
????????19:?getstatic?????#12?????????????????//?Field?INSTANCE:LSingleton;取出字段INSTANCE的name值
????????22:?aastore?????????
????????23:?putstatic?????#1??????????????????//?Field?$VALUES:[LSingleton;將局部變量表中的枚舉項的name值都依次放入$VALUES數(shù)組中
????????26:?return
??????LineNumberTable:
????????line?2:?0
????????line?1:?13
}
其實編譯完的字節(jié)碼是給JVM看的,JVM只需要無腦順序往下執(zhí)行即可。多的方面就涉及JVM很多內(nèi)容了,和本文主題無關,后續(xù)會另開一篇講字節(jié)碼指令。這里大家主要看static代碼塊中有哪些指令,明白枚舉為什么會線程安全即可。
