Java超詳細(xì)講解設(shè)計(jì)模式之一的單例模式
單例模式
單例模式顧名思義就是單一的實(shí)例,涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有一個(gè)對(duì)象被創(chuàng)建,并且提供一種可以訪問這個(gè)對(duì)象的方式,可以直接訪問,不需要實(shí)例化該類的對(duì)象。
單例模式的特點(diǎn):
1.單例類只能有一個(gè)實(shí)例
2.這個(gè)實(shí)例必須由單例類自己創(chuàng)建
3.單例類需要提供給外界訪問這個(gè)實(shí)例
單例模式的作用:
單例模式主要為了保證在Java應(yīng)用程序中,一個(gè)類只有一個(gè)實(shí)例存在。
1.單例模式的結(jié)構(gòu)
單例模式主要有以下角色:
- 單例類
只能創(chuàng)建一個(gè)實(shí)例的類
- 訪問類
測(cè)試類,就是使用單例類的類
2.單例模式的實(shí)現(xiàn)
2.1餓漢式
餓漢式:類加載時(shí)創(chuàng)建該單實(shí)例類對(duì)象
1.餓漢式-方式1 靜態(tài)成員變量
創(chuàng)建 餓漢式靜態(tài)成員變量 單例類
public class Demo1 { /** *私有構(gòu)造方法 讓外界不能創(chuàng)建該類對(duì)象 */ private Demo1(){} /** * 在類中創(chuàng)建該本類對(duì)象 static是由于外界獲取該類對(duì)象的方法getInstance()是 static * 這個(gè)對(duì)象instance就是靜態(tài)成員變量 */ private static Demo1 instance = new Demo1(); /** * 提供一個(gè)公共的訪問方式,讓外界可以獲取該類的對(duì)象 static是因?yàn)橥饨绮恍枰獎(jiǎng)?chuàng)建對(duì)象,直接通過類訪問 */ public static Demo1 getInstance(){ return instance; } }
創(chuàng)建 餓漢式靜態(tài)成員變量 測(cè)試類(訪問類)
public class Test1 { public static void main(String[] args) { //創(chuàng)建demo1類的對(duì)象 這個(gè)時(shí)候就無法通過new創(chuàng)建了,因?yàn)閐emo1的構(gòu)造方法是私有的 Demo1 instance = Demo1.getInstance(); Demo1 instance1 = Demo1.getInstance(); //判斷兩個(gè)對(duì)象是否是同一個(gè) System.out.println(instance == instance1); } }
輸出true 表明是同一個(gè)對(duì)象,指向同一塊內(nèi)存地址,這樣我們就保證了Demo1單例類只有一個(gè)對(duì)象被創(chuàng)建
2.餓漢式-方式2 靜態(tài)代碼塊
創(chuàng)建 餓漢式靜態(tài)代碼塊 單例類
public class Demo2 { //餓漢式單例類 靜態(tài)代碼塊 /** *私有構(gòu)造方法 讓外界不能創(chuàng)建該類對(duì)象 */ private Demo2(){} /** * 聲明一個(gè)靜態(tài)的成員變量instance但是不賦值(不創(chuàng)建對(duì)象) * 沒有為instance賦值,默認(rèn)為null */ private static Demo2 instance; /** * 在靜態(tài)代碼快中為instance賦值(創(chuàng)建對(duì)象) */ static { instance = new Demo2(); } /** * 提供一個(gè)公共的訪問方式,讓外界可以獲取該類的對(duì)象 static是因?yàn)橥饨绮恍枰獎(jiǎng)?chuàng)建對(duì)象,直接通過類訪問 */ public static Demo2 getInstance(){ return instance; } }
創(chuàng)建 餓漢式靜態(tài)代碼塊 測(cè)試類
public class Test2 { public static void main(String[] args) { Demo2 instance = Demo2.getInstance(); Demo2 instance1 = Demo2.getInstance(); System.out.println(instance == instance1); } }
輸出true 表明是同一個(gè)對(duì)象,指向同一塊內(nèi)存地址,這樣我們就保證了Demo2單例類只有一個(gè)對(duì)象被創(chuàng)建
3.餓漢式-方式3(枚舉方式)
枚舉類實(shí)現(xiàn)單例模式是十分推薦的一種單例實(shí)現(xiàn)模式,由于枚舉類型是線程安全的,并且只會(huì)加載一次,這是十分符合單例模式的特點(diǎn)的,枚舉的寫法很簡(jiǎn)單,而且枚舉方式是所有單例實(shí)現(xiàn)中唯一一個(gè)不會(huì)被破環(huán)的單例實(shí)現(xiàn)模式
單例類
//枚舉方式創(chuàng)建單例 public enum Singleton { INSTANCE; }
測(cè)試類
public class Test1 { public static void main(String[] args) { Singleton instance = Singleton.INSTANCE; Singleton instance1 = Singleton.INSTANCE; System.out.println(instance == instance1); //輸出 true } }
注意:
? 由于枚舉方式是餓漢式,因此根據(jù)餓漢式的特點(diǎn),枚舉方式也會(huì)造成內(nèi)存浪費(fèi),但是在不考慮內(nèi)存問題下,枚舉方式是首選,畢竟實(shí)現(xiàn)最簡(jiǎn)單了
2.2懶漢式
懶漢式:類加載時(shí)不會(huì)創(chuàng)建該單實(shí)例對(duì)象,首次使用該對(duì)象時(shí)才會(huì)創(chuàng)建
1.懶漢式-方式1 (線程不安全)
public class Demo3 { /** *私有構(gòu)造方法 讓外界不能創(chuàng)建該類對(duì)象 */ private Demo3(){} /** * 在類中創(chuàng)建該本類對(duì)象 static是由于外界獲取該類對(duì)象的方法getInstance()是 static * 沒有進(jìn)行賦值(創(chuàng)建對(duì)象) */ private static Demo3 instance; /** * 提供一個(gè)公共的訪問方式,讓外界可以獲取該類的對(duì)象 static是因?yàn)橥饨绮恍枰獎(jiǎng)?chuàng)建對(duì)象,直接通過類訪問 */ public static Demo3 getInstance(){ //在首次使用該對(duì)象時(shí)創(chuàng)建,因此instance賦值也就是對(duì)象創(chuàng)建 就是在外界獲取該單例類的方法getInstance()中創(chuàng)建 instance = new Demo3(); return instance; } }
public class Test3 { public static void main(String[] args) { Demo3 instance = Demo3.getInstance(); Demo3 instance1 = Demo3.getInstance(); //判斷兩個(gè)對(duì)象是否是同一個(gè) System.out.println(instance == instance1); } }
輸出結(jié)果為false,表明我們創(chuàng)建懶漢式單例失敗了。是因?yàn)槲覀冊(cè)谡{(diào)用getInstance()時(shí)每次調(diào)用都會(huì)new一個(gè)實(shí)例對(duì)象,那么也就必然不可能相等了。
// 如果instance為null,表明還沒有創(chuàng)建該類的對(duì)象,那么就進(jìn)行創(chuàng)建 if(instance == null){ instance = new Demo3(); } //如果instance不為null,表明已經(jīng)創(chuàng)建過該類的對(duì)象,根據(jù)單例類只能創(chuàng)建一個(gè)對(duì)象的特點(diǎn),因此 //我們直接返回instance return instance; }
注意:
我們?cè)跍y(cè)試是只是單線程,但是在實(shí)際應(yīng)用中必須要考慮到多線程的問題。我們假設(shè)一種情況,線程1進(jìn)入if判斷然后還沒來得及創(chuàng)建instance,這個(gè)時(shí)候線程1失去了cpu的執(zhí)行權(quán)變?yōu)樽枞麪顟B(tài),線程2獲取cpu執(zhí)行權(quán),然后進(jìn)行if判斷此時(shí)instance還是null,因此線程2為instance賦值創(chuàng)建了該單例對(duì)象,那么等到線程1再次獲取cpu執(zhí)行權(quán),也進(jìn)行了instance賦值創(chuàng)建了該單例對(duì)象,單例模式被破壞。
2.懶漢式-方式2 (線程安全)
我們可以通過加synchronized同步鎖的方式保證單例模式在多線程下依舊有效
public static synchronized Demo3 getInstance(){ //在首次使用該對(duì)象時(shí)創(chuàng)建,因此instance賦值也就是對(duì)象創(chuàng)建 就是在外界獲取該單例類的方法getInstance()中創(chuàng)建 // 如果instance為null,表明還沒有創(chuàng)建該類的對(duì)象,那么就進(jìn)行創(chuàng)建 if(instance == null){ instance = new Demo3(); } //如果instance不為null,表明已經(jīng)創(chuàng)建過該類的對(duì)象,根據(jù)單例類只能創(chuàng)建一個(gè)對(duì)象的特點(diǎn),因此我們直接返回instance return instance; }
注意:
雖然保證了線程安全問題,但是在getInstance()方法上添加了synchronized關(guān)鍵字,導(dǎo)致該方法執(zhí)行效率很低(這是加鎖的一個(gè)常見問題)。其實(shí)我們可以很容易發(fā)現(xiàn),我們只是在判斷instance時(shí)需要解決多線程的安全問題,而沒必要在getInstance()上加鎖
3.懶漢式-方式3(雙重檢查鎖)
對(duì)于getInstance()方法來說,絕大部分的操作都是讀操作,讀操作是線程安全的,沒必要讓每個(gè)線程必須持有鎖才能調(diào)用該方法,我們可以調(diào)整加鎖的時(shí)機(jī)。
public class Demo4 { /** *私有構(gòu)造方法 讓外界不能創(chuàng)建該類對(duì)象 */ private Demo4(){} /** * * 沒有進(jìn)行賦值(創(chuàng)建對(duì)象) 只是聲明了一個(gè)該類的變量 */ private static Demo4 instance; /** * 提供一個(gè)公共的訪問方式,讓外界可以獲取該類的對(duì)象 static是因?yàn)橥饨绮恍枰獎(jiǎng)?chuàng)建對(duì)象,直接通過類訪問 */ public static Demo4 getInstance(){ // (第一次判斷)如果instance為null,表明還沒有創(chuàng)建該類的對(duì)象,那么就進(jìn)行創(chuàng)建 if(instance == null){ synchronized (Demo4.class){ //第二次判斷 如果instance不為null if(instance == null){ instance = new Demo4(); } } } //如果instance不為null,表明已經(jīng)創(chuàng)建過該單例類的對(duì)象,不需要搶占鎖,直接返回 return instance; } }
雙重檢查鎖模式完美的解決了單例、性能、線程安全問題,但是只是這樣還是有問題的…
JVM在創(chuàng)建對(duì)象時(shí)會(huì)進(jìn)行優(yōu)化和指令重排,在多線程下可能會(huì)發(fā)生空指針異常的問題,可以使用volatile關(guān)鍵字,volatile可以保證可見性和有序性。
private static volatile Demo4 instance;
如果發(fā)生指令重排 2 和 3 的步驟顛倒,那么instance會(huì)指向一塊虛無的內(nèi)存(也有可能是有數(shù)據(jù)的一塊內(nèi)存)
完整代碼
public class Demo4 { /** *私有構(gòu)造方法 讓外界不能創(chuàng)建該類對(duì)象 */ private Demo4(){} /** * volatile可以保證有序性 * 沒有進(jìn)行賦值(創(chuàng)建對(duì)象) 只是聲明了一個(gè)該類的變量 */ private static volatile Demo4 instance; /** * 提供一個(gè)公共的訪問方式,讓外界可以獲取該類的對(duì)象 static是因?yàn)橥饨绮恍枰獎(jiǎng)?chuàng)建對(duì)象,直接通過類訪問 */ public static Demo4 getInstance(){ // (第一次判斷)如果instance為null,表明還沒有創(chuàng)建該類的對(duì)象,那么就進(jìn)行創(chuàng)建 if(instance == null){ synchronized (Demo4.class){ //第二次判斷 如果instance不為null if(instance == null){ instance = new Demo4(); } } } //如果instance不為null,表明已經(jīng)創(chuàng)建過該單例類的對(duì)象,不需要搶占鎖,直接返回 return instance; } }
4.懶漢式-4 (靜態(tài)內(nèi)部類)
靜態(tài)內(nèi)部類單例模式中實(shí)例由內(nèi)部類創(chuàng)建,由于JVM在加載外部類的過程中,是不會(huì)加載靜態(tài)內(nèi)部類的,只有內(nèi)部類的屬性/方法被調(diào)用時(shí)才會(huì)被加載,并初始化其靜態(tài)屬性。靜態(tài)屬性由于被final修飾,保證只被實(shí)例化一次,并且嚴(yán)格保證實(shí)例化順序。
創(chuàng)建單例類
public class Singleton { private Singleton(){} /** *定義一個(gè)靜態(tài)內(nèi)部類 */ private static class SingletonHolder{ //在靜態(tài)內(nèi)部類中創(chuàng)建外部類的對(duì)象 private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.INSTANCE; } }
創(chuàng)建測(cè)試類
public class Test4 { public static void main(String[] args) { Singleton instance = Singleton.getInstance(); Singleton instance1 = Singleton.getInstance(); //判斷兩個(gè)對(duì)象是否是同一個(gè) System.out.println(instance == instance1); } }
注意:
? 第一次加載Singleton類時(shí)不會(huì)去初始化INSTANCE,只有在調(diào)用getInstance()方法時(shí),JVM加載SingletonHolder并初始化INSTANCE,這樣可以保證線程安全,并且Singleton類的唯一性
? 靜態(tài)內(nèi)部類單例模式是一種開源項(xiàng)目比較常用的單例模式,在沒有任何加鎖的情況下保證多線程的安全,并且沒有任何性能和空間上的浪費(fèi)
3.單例模式的破壞
單例模式最重要的一個(gè)特點(diǎn)就是只能創(chuàng)建一個(gè)實(shí)例對(duì)象,那么如果能使單例類能創(chuàng)建多個(gè)就破壞了單例模式(除了枚舉方式)破壞單例模式的方式有兩種:
3.1序列化和反序列化
從以上創(chuàng)建單例模式的方式中任選一種(除枚舉方式),例如靜態(tài)內(nèi)部類方式
//記得要實(shí)現(xiàn)Serializable序列化接口 public class Singleton implements Serializable { private Singleton(){} /** *定義一個(gè)靜態(tài)內(nèi)部類 */ private static class SingletonHolder{ //在靜態(tài)內(nèi)部類中創(chuàng)建外部類的對(duì)象 private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.INSTANCE; } }
測(cè)試類
public class Test1 { public static void main(String[] args) throws IOException { writeObjectToFile(); } /** * 向文件中寫數(shù)據(jù)(對(duì)象) * @throws IOException */ public static void writeObjectToFile() throws IOException { //1.獲取singleton對(duì)象 Singleton instance = Singleton.getInstance(); //2.創(chuàng)建對(duì)象輸出流對(duì)象 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\1.txt")); //3.寫對(duì)象 oos.writeObject(instance); //4.釋放資源 oos.close(); } }
在d盤根目錄下出現(xiàn)一個(gè)文件1.txt由于數(shù)據(jù)是序列化后的 咱也看不懂
然后我們從這個(gè)文件中讀取instance對(duì)象
public static void main(String[] args) throws Exception { // writeObjectToFile(); readObjectFromFile(); readObjectFromFile(); } /** * 從文件中讀數(shù)據(jù)(對(duì)象) * @throws Exception */ public static void readObjectFromFile() throws Exception { //1.創(chuàng)建對(duì)象輸入流對(duì)象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:\\1.txt")); //2.讀對(duì)象 Singleton instance = (Singleton) ois.readObject(); System.out.println(instance); //3.釋放資源 ois.close(); }
輸出結(jié)果不相同,結(jié)論為:序列化破壞了單例模式,兩次讀的對(duì)象不一樣了
com.xue.demo01.Singleton@2328c243
com.xue.demo01.Singleton@bebdb06
解決方案
在singleton中添加readResolve方法
/** * 當(dāng)進(jìn)行反序列化時(shí),會(huì)自動(dòng)調(diào)用該方法,將該方法的返回值直接返回 * @return */ public Object readResolve(){ return SingletonHolder.INSTANCE; }
重新進(jìn)行寫和讀,發(fā)現(xiàn)兩次讀的結(jié)果是相同的,解決了序列化破壞單例模式的問題
為什么在singleton單例類中添加readResolve方法就可以解決序列化破壞單例的問題呢,我們?cè)贠bjectInputStream源碼中在readOrdinaryObject方法中
private Object readOrdinaryObject(boolean unshared) throws IOException{ //代碼段 Object obj; try { //isInstantiable如果一個(gè)實(shí)現(xiàn)序列化的類在運(yùn)行時(shí)被實(shí)例化就返回true //desc.newInstance()會(huì)通過反射調(diào)用無參構(gòu)造創(chuàng)建一個(gè)新的對(duì)象 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 && //hasReadResolveMethod 如果實(shí)現(xiàn)序列化接口的類中定義了readResolve方法就返回true desc.hasReadResolveMethod()) { //通過反射的方式調(diào)用被反序列化類的readResolve方法 Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } //代碼段 }
3.2反射
從以上創(chuàng)建單例模式的方式中任選一種(除枚舉方式),例如靜態(tài)內(nèi)部類方式
測(cè)試類
public class Test1 { public static void main(String[] args) throws Exception { //1.獲取Singleton的字節(jié)碼對(duì)象 Class<Singleton> singletonClass = Singleton.class; //2.獲取無參構(gòu)造方法對(duì)象 Constructor cons = singletonClass.getDeclaredConstructor(); //3.取消訪問檢查 cons.setAccessible(true); //4.反射創(chuàng)建對(duì)象 Singleton instance1 = (Singleton) cons.newInstance(); Singleton instance2 = (Singleton) cons.newInstance(); System.out.println(instance1 == instance2); //輸出false 說明反射破壞了單例模式 } }
解決方案:
public class Singleton { //static是為了都能訪問 private static boolean flag = false; private Singleton() { //加上同步鎖,防止多線程并發(fā)問題 synchronized (Singleton.class) { //判斷flag是否為true,如果為true說明不是第一次創(chuàng)建,拋異常 if (flag) { throw new RuntimeException("不能創(chuàng)建多個(gè)對(duì)象"); } //flag的值置為true flag = true; } } /** *定義一個(gè)靜態(tài)內(nèi)部類 */ private static class SingletonHolder{ //在靜態(tài)內(nèi)部類中創(chuàng)建外部類的對(duì)象 private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.INSTANCE; } }
這樣就不能通過之前的反射方式破壞單例模式了,但是如果通過反射修改flag的值也是可以破壞單例模式的,但是這樣可以防止意外反射破壞單例模式,如果刻意破壞是很難防范的,畢竟反射太強(qiáng)了??????
到此這篇關(guān)于Java超詳細(xì)講解設(shè)計(jì)模式之一的單例模式的文章就介紹到這了,更多相關(guān)Java 單例模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)順序表示例
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)順序表示例,需要的朋友可以參考下2014-03-03解決springboot讀取application.properties中文亂碼問題
初用properties,讀取java properties文件的時(shí)候如果value是中文,會(huì)出現(xiàn)亂碼的問題,所以本文小編將給大家介紹如何解決springboot讀取application.properties中文亂碼問題,需要的朋友可以參考下2023-11-11java多線程編程之Synchronized關(guān)鍵字詳解
這篇文章主要為大家詳細(xì)介紹了java多線程編程之Synchronized關(guān)鍵字,感興趣的朋友可以參考一下2016-05-05mybatisplus?@Select注解中拼寫動(dòng)態(tài)sql異常問題的解決
這篇文章主要介紹了mybatisplus?@Select注解中拼寫動(dòng)態(tài)sql異常問題的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12