Java設(shè)計模式之單例模式示例詳解
0.概述
為什么要使用單例模式?
在我們的系統(tǒng)中,有一些對象其實(shí)我們只需要一個,比如說:線程池、緩存、對話框、注冊表、日志對象、充當(dāng)打印機(jī)、顯卡等設(shè)備驅(qū)動程序的對象。事實(shí)上,這一類對象只能有一個實(shí)例,如果制造出多個實(shí)例就可能會導(dǎo)致一些問題的產(chǎn)生,比如:程序的行為異常、資源使用過量、或者不一致性的結(jié)果。因此這里需要用到單例模式
使用單例模式的好處?
對于頻繁使用的對象,可以省略創(chuàng)建對象所花費(fèi)的時間,這對于那些重量級對象而言,是非??捎^的一筆系統(tǒng)開銷
由于new 操作的次數(shù)減少,因而對系統(tǒng)內(nèi)存的使用頻率也會降低,這將減輕 GC 壓力,縮短 GC 停頓時間
1.餓漢式
1.1 餓漢式單例實(shí)現(xiàn)
實(shí)例會提前創(chuàng)建:
/** * 餓漢式 * * @author xppll * @date 2021/12/24 21:21 */ public class Singleton1 implements Serializable { //構(gòu)造私有 private Singleton1() { System.out.println("private Singleton1()"); } //唯一實(shí)例 private static final Singleton1 INSTANCE = new Singleton1(); //獲得實(shí)例方法 public static Singleton1 getINSTANCE() { return INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
測試:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) { //觸發(fā)Singleton1類的初始化,會為類的靜態(tài)變量賦予正確的初始值,單例對象就會被創(chuàng)建! Singleton1.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton1.getINSTANCE()); System.out.println(Singleton1.getINSTANCE()); } } //輸出: private Singleton1() otherMethod() ----------------------------------- singleton.Singleton1@10bedb4 singleton.Singleton1@10bedb4
1.2 破壞單例的幾種情況
1.反射破壞單例
2.反序列化破壞單例
3.Unsafe破壞單例
演示:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException { //觸發(fā)Singleton1類的初始化,會為類的靜態(tài)變量賦予正確的初始值,單例對象就會被創(chuàng)建! Singleton1.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton1.getINSTANCE()); System.out.println(Singleton1.getINSTANCE()); //反射破壞單例 reflection(Singleton1.class); //反序列化破壞單例 serializable(Singleton1.getINSTANCE()); //Unsafe破壞單例 unsafe(Singleton1.class); } //反射破壞單例 private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { //得到無參 Constructor<?> constructor = clazz.getDeclaredConstructor(); //將此對象的 accessible 標(biāo)志設(shè)置為指示的布爾值,即設(shè)置其可訪問性 constructor.setAccessible(true); //創(chuàng)建實(shí)例 System.out.println("反射創(chuàng)建實(shí)例:" + constructor.newInstance()); } //反序列化破壞單例 private static void serializable(Object instance) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); //序列化 oos.writeObject(instance); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); //反序列化 System.out.println("反序列化創(chuàng)建示例:" + ois.readObject()); } //Unsafe破壞單例 private static void unsafe(Class<?> clazz) throws InstantiationException { Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz); System.out.println("Unsafe 創(chuàng)建實(shí)例:" + o); } }
結(jié)果:
可以看出三種方式都會破壞單例!
1.3 預(yù)防單例的破壞
預(yù)防反射破壞單例
在構(gòu)造方法中加個判斷即可:
//構(gòu)造私有 private Singleton1() { //防止反射破壞單例 if(INSTANCE!=null){ throw new RuntimeException("單例對象不能重復(fù)創(chuàng)建"); } System.out.println("private Singleton1()"); }
預(yù)防反序列化破壞單例
在Singleton1()
中重寫readResolve
方法:
//重寫這個方法,如果序列化了,就會返回這個,不會返回反序列化的對象 public Object readResolve(){ return INSTANCE; }
Unsafe破壞單例無法預(yù)防
2.枚舉餓漢式
2.1 枚舉單例實(shí)現(xiàn)
枚舉實(shí)現(xiàn)單例:
/** * 枚舉實(shí)現(xiàn)單例 * * @author xppll * @date 2021/12/24 22:23 */ public enum Singleton2 { INSTANCE; //枚舉的構(gòu)造方法默認(rèn)是private的,可以不寫 Singleton2() { System.out.println("private Singleton2()"); } //重寫toString方法 @Override public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } //獲得實(shí)例方法(這個可以不要,枚舉變量都是public的) public static Singleton2 getInstance() { return INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
測試:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException { //觸發(fā)Singleton2類的初始化,會為類的靜態(tài)變量賦予正確的初始值,單例對象就會被創(chuàng)建! Singleton2.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton2.getInstance()); System.out.println(Singleton2.getInstance()); } } //輸出: private Singleton2() otherMethod() ----------------------------------- singleton.Singleton2@2de80c singleton.Singleton2@2de80c
可以看出當(dāng)調(diào)用otherMethod()
時,就會觸發(fā)類的加載,枚舉對象就會創(chuàng)建,所以枚舉實(shí)現(xiàn)單例是餓漢式的
2.2 破壞單例
枚舉類實(shí)現(xiàn)單例的好處:
1.反序列化無法破壞枚舉單例
2.反射無法破壞枚舉單例
栗子:
需要先修改反射破壞代碼,枚舉需要有參構(gòu)造
public class TestSingleton { public static void main(String[] args) throws Exception { Singleton5.otherMethod(); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); System.out.println(Singleton5.getInstance()); System.out.println(Singleton5.getInstance()); //反序列化破壞單例 serializable(Singleton2.getInstance()); //Unsafe破壞單例 unsafe(Singleton2.class); //反射破壞單例 reflection(Singleton2.class); } private static void unsafe(Class<?> clazz) throws InstantiationException { Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz); System.out.println("Unsafe 創(chuàng)建實(shí)例:" + o); } private static void serializable(Object instance) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(instance); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); System.out.println("反序列化創(chuàng)建實(shí)例:" + ois.readObject()); } private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class); constructor.setAccessible(true); System.out.println("反射創(chuàng)建實(shí)例:" + constructor.newInstance()); } }
結(jié)果:
可以看出
1.反射是無法創(chuàng)建枚舉對象!無法破壞枚舉單例
2.反序列化也不會破壞枚舉單例!
3.Unsafe依然會破壞!
3.懶漢式
實(shí)現(xiàn)代碼如下:
/** * 懶漢式 * * @author xppll * @date 2021/12/25 08:34 */ public class Singleton3 implements Serializable { //構(gòu)造私有 private Singleton3() { System.out.println("private Singleton3()"); } //唯一實(shí)例 private static Singleton3 INSTANCE = null; public static Singleton3 getInstance() { //第一次調(diào)用的時候才創(chuàng)建 if (INSTANCE == null) { INSTANCE = new Singleton3(); } return INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
測試:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException { Singleton3.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton3.getInstance()); System.out.println(Singleton3.getInstance()); } }
結(jié)果:
可以看出只有在第一次調(diào)用getInstance()
時才會創(chuàng)建唯一的單例對象,因此是懶漢式的。
但是這種方式在多線程環(huán)境下是會有問題的,可能多個線程會同時執(zhí)行INSTANCE = new Singleton3();
。因此這里需要在getInstance()
方法上加上synchronized
關(guān)鍵字保證多線程下的正確性:
public static synchronized Singleton3 getInstance() { //第一次調(diào)用的時候才創(chuàng)建 if (INSTANCE == null) { INSTANCE = new Singleton3(); } return INSTANCE; }
但是這種方法是有問題的,第一次創(chuàng)建完對象后,以后的操作是不需要在加鎖的,所以這種方式會影響性能!
我們的目標(biāo)應(yīng)該是第一次創(chuàng)建單例的時候給予保護(hù),后續(xù)操作則不需要加鎖保護(hù)!
4.雙檢鎖懶漢式
針對上面的問題,這里給出第四種方法雙檢鎖懶漢式進(jìn)行優(yōu)化:
/** * 雙檢鎖懶漢式 * * @author xppll * @date 2021/12/25 08:53 */ public class Singleton4 { //構(gòu)造私有 private Singleton4() { System.out.println("private Singleton4()"); } //唯一實(shí)例 //這里volatile的作用是保證共享變量有序性! private static volatile Singleton4 INSTANCE = null; //雙檢鎖優(yōu)化 public static synchronized Singleton4 getInstance() { //實(shí)例沒創(chuàng)建,才會進(jìn)入內(nèi)部的 synchronized 代碼塊,提高性能,防止每次都加鎖 if (INSTANCE == null) { //可能第一個線程在synchronized 代碼塊還沒創(chuàng)建完對象時,第二個線程已經(jīng)到了這一步,所以里面還需要加上判斷 synchronized (Singleton4.class) { //也許有其他線程已經(jīng)創(chuàng)建實(shí)例,所以再判斷一次 if (INSTANCE == null) { INSTANCE = new Singleton4(); } } } return INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
5.內(nèi)部類懶漢式
內(nèi)部類懶漢式單例實(shí)現(xiàn):
/** * 內(nèi)部類懶漢式 * * @author xppll * @date 2021/12/25 09:24 */ public class Singleton5 { //構(gòu)造私有 private Singleton5() { System.out.println("private Singleton5()"); } //靜態(tài)內(nèi)部類實(shí)現(xiàn)懶漢式單例,靜態(tài)變量的創(chuàng)建會放在靜態(tài)代碼塊里執(zhí)行,jvm會保證其線程安全 //只有第一次用到內(nèi)部類時,才會初始化創(chuàng)建單例 private static class Holder { static Singleton5 INSTANCE = new Singleton5(); } //獲得實(shí)例方法 public static Singleton5 getInstance() { return Holder.INSTANCE; } //其他方法 public static void otherMethod() { System.out.println("otherMethod()"); } }
測試:
/** * @author xppll * @date 2021/12/24 21:28 */ public class TestSingleton { public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException { Singleton5.otherMethod(); System.out.println("-----------------------------------"); System.out.println(Singleton5.getInstance()); System.out.println(Singleton5.getInstance()); } }
結(jié)果:
可以看出內(nèi)部類實(shí)現(xiàn)單例也是懶漢式的!
6.JDK中單例的體現(xiàn)
Runtime 體現(xiàn)了餓漢式單例
System類下的Console 體現(xiàn)了雙檢鎖懶漢式單例
Collections 中的 EmptyNavigableSet內(nèi)部類懶漢式單例
Collections 中的ReverseComparator.REVERSE_ORDER 內(nèi)部類懶漢式單例
Comparators.NaturalOrderComparator.INSTANCE 枚舉餓漢式單例
到此這篇關(guān)于Java設(shè)計模式之單例模式示例詳解的文章就介紹到這了,更多相關(guān)Java單例模式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java自定義日志輸出文件(log4j日志文件輸出多個自定義日志文件)
打印日志的在程序中是必不可少的,如果需要將不同的日志打印到不同的地方,則需要定義不同的Appender,然后定義每一個Appender的日志級別、打印形式和日志的輸出路徑,下面看一個示例吧2014-01-01Java之獲取客戶端真實(shí)IP地址的實(shí)現(xiàn)
在開發(fā)工作中,我們常常需要獲取客戶端的IP,本文主要介紹了Jav之獲取客戶端真實(shí)IP地址的實(shí)現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-12-12Springboot 讀取自定義pro文件注入static靜態(tài)變量方式
這篇文章主要介紹了Springboot 讀取自定義pro文件注入static靜態(tài)變量方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Java?Stream?流中?Collectors.toMap?的用法詳解
這篇文章主要介紹了Stream?流中?Collectors.toMap?的用法,Collectors.toMap()方法是把List轉(zhuǎn)Map的操作,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2024-01-01Javas使用Redlock實(shí)現(xiàn)分布式鎖過程解析
這篇文章主要介紹了Javas使用Redlock實(shí)現(xiàn)分布式鎖過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-08-08