回字有四种写法,那你知道单例有五种写法吗
點擊上方?好好學java?,選擇?星標?公眾號
轉自:RudeCrab,
鏈接:jianshu.com/p/540c1085c5de
基本介紹
單例模式(Singleton)應該是大家接觸的第一個設計模式,其寫法相較于其他的設計模式來說并不復雜,核心理念也非常簡單:程序從始至終只有同一個該類的實例對象。
舉一個耳熟能詳的例子,比如 LOL 中的大龍,一場游戲下來無論如何只有一只,所以該類只能被實例化一次。再舉一個我們應用程序開發中常見的例子,Spring 框架中的 Bean 作用范圍默認也是單例的。
我相信大家都知道單例的兩種最基本的寫法:餓漢式和懶漢式。但是這兩種寫法都有其弊端所在,除了這兩種寫法外其實還有幾種寫法。此時耳邊仿佛聽到孔乙己的聲音:
“對呀對呀!......回字有四樣寫法,你知道么?”。?
我愈不耐煩了,努著嘴走遠。孔乙己剛用指甲蘸了酒,想在柜上寫字,見我毫不熱心,便又嘆一口氣,顯出極惋惜的樣子........
大家先別著急走,回字的四樣寫法沒必要知道,單例的五種寫法還是有必要曉得滴,其他的不說,至少面試的時候還能和面試官吹下是不,況且這幾種寫法也不是純吊書袋,了解過后還是能幫助我們理解其設計思想滴。所以接下來咱們由淺入深,從最容易的寫法開始,一步一步的帶大家掌握單例模式!
寫法介紹
1. 餓漢式
話不多說,先直接上最簡單的寫法,然后咱再慢慢剖析:
public class Signleton01 {// 私有構造函數,防止別人實例化private Signleton01(){}// 靜態屬性,指向一個實例化對象private static final Signleton01 INSTANCE = new Signleton01();// 公共方法,以便別人獲取到實例化對象屬性public static Signleton01 getINSTANCE() {return INSTANCE;} }單例模式三元素
一個單例模式就這樣寫完了,簡直不要太簡單。類里面一共就三個元素:
私有構造函數,防止別人實例化;
靜態屬性,指向一個實例化對象;
公共方法,以便別人獲取到實例化對象屬性。
這三個元素就是單例模式的核心,單例無論哪種寫法,都離不開這三個元素。
這三個元素也很好理解,別人想要用我這個類的實例對象就只能通過我提供的 getINSTANCE(),他想 new 也 new 不了第二個對象,自然而然就保證了該類只有唯一對象。我們可以做個試驗,跑100個線程同時獲取該類的實例對象,然后打印出對象的 hashCode,看看到底是不是獲取的同一個對象:
public static void main(String[] args) {for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println(Signleton01.getINSTANCE().hashCode());}).start();} }結果如下:
... 834649078 834649078 834649078 834649078 834649078 ...嗯,全部都是同一個對象。
優缺點
優點:寫法簡單,線程安全;
缺點:消耗資源,即使程序從沒有用到過該類對象,該類也會初始化一個對象出來。
所以為了解決餓漢式的這個缺點, 我們就引出了第二種寫法,懶漢式!
2. 懶漢式
基本寫法
public class Singleton02 {// 私有構造函數,防止別人實例化private Singleton02() {}// 靜態屬性,指向一個實例化對象(注意,這里沒有實例化對象哦)private static Singleton02 INSTANCE;// 公共方法,以便別人獲取到實例化對象屬性public static Singleton02 getINSTANCE() {if (INSTANCE == null) {INSTANCE = new Singleton02();}return INSTANCE;} }懶漢式的和餓漢式最大的區別是什么呢,就是只有在調用 getINSTANCE 的時候,才會創建實例。如果你從來沒調用過,那么就不實例化對象。這個就比餓漢式更加節約資源,不過這種寫法并不是懶漢式的完善寫法,它有一個非常大的問題,就是線程不同步!我們可以按照之前那種方式創建100個線程測試一下結果:
... 1851261656 868907500 988762476 1031371881 593800070 ...可以看到這線程一同時拿,拿的都不是同一個對象,這完全就破壞了單例模式。因為很多線程在對象沒有初始化前就進入到了 if (INSTANCE == null) 判斷語句塊里,自然而然就會 new 出不同的對象了。要解決這個線程不安全問題,就得上線程鎖!
3. synchronized 寫法
public?class?Singleton02?{private?Singleton02()?{}private static Singleton02 INSTANCE;// 注意,這里靜態方法加了synchronized關鍵字public synchronized static Singleton02 getINSTANCE() {if (INSTANCE == null) {INSTANCE = new Singleton02();}return INSTANCE;} }當我們在靜態方法加上 synchronized 關鍵字后,就可以保證這個方法在同一時間只會有一個線程能成功調用,也就順理成章的解決了線程不安全問題。我們還是測試一下:
... 1226880356 1226880356 1226880356 1226880356 1226880356 ...不管多少個線程,拿到的都是同一個對象,達到了單例的要求!
優缺點
懶漢式連基本的線程安全都不能保證,就不做討論了,我們這里主要說的是 synchronized 寫法:
優點:寫法簡單,節約資源(只有需要該對象的時候才會實例化);
缺點:耗性能。
要知道每一次調用 getINSTANCE() 方法時都會上鎖,這是非常耗性能的。那么為了解決這個好性能的問題,我們又引申出接下來的一種寫法。
4. 雙重檢測
每一次調用 getINSTANCE() 方法都會上鎖,這是完全沒有必要的嘛,因為只有對象還沒有實例化的時候我才需要上鎖以保證線程安全。對象都實例化了,自然也不用擔心后續的調用會 new 出新的對象。所以我們這個鎖,可以加在 if (INSTANCE == null) 判斷語句塊里面:
public class Singleton03 {private?Singleton03()?{}private static Singleton03 INSTANCE;public static Singleton03 getINSTANCE() {if (INSTANCE == null) {// 只有在對象還沒有實例化的時候才上鎖synchronized (Singleton03.class) {INSTANCE = new Singleton03();}}return INSTANCE;} }這樣就能節約一些性能,但是這樣并沒有做到線程安全哦!因為很多線程進入到if ?(INSTANCE == null) 判斷語句后,雖說是因為鎖不能同時 new 對象了,但是如果鎖一旦釋放,那么其他線程依然會執行到 INSTANCE = new Singleton03() 語句,從而破壞了單例。所以在 synchronized 代碼塊內還要加一層判斷:
public class Singleton03 {private?Singleton03()?{}// 注意,使用雙重檢驗寫法要加上volatile關鍵字,避免指令重排(有個印象就行,這不是本文的重點)private static volatile Singleton03 INSTANCE;public static Singleton03 getINSTANCE() {if (INSTANCE == null) {// 只有在對象還沒有實例化的時候才上鎖synchronized (Singleton03.class) {// 額外加一層判斷if (INSTANCE == null) {INSTANCE = new Singleton03();}}}return INSTANCE;} }synchronized 代碼塊外面一層判斷,里面一層判斷,就是有名的雙重檢測(DCL)了!里面的這一層判斷加了之后呢,第一個線程的鎖一旦釋放也不用擔心了,因為此時對象已經實例化,后續的線程也執行不了 new 語句,從而保證了線程安全!
優缺點
優點:節約資源(只有需要該對象的時候才會實例化);
缺點:寫法復雜,耗性能(還是上了鎖,還是耗性能)。
雖然雙重校驗比 synchronized 懶漢式寫法減少了很多鎖性能消耗,但畢竟還是上了鎖,所以為了解決這個鎖性能消耗問題了,又引申出下一種寫法。
5. 內部類
話不多說,直接上代碼:
public class Singleton04 {// 老套路,將構造函數私有化private Singleton04() {}// 聲明一個內部類,內部類里持有實例的引用private static class Inner {public static final Singleton04 INSTANCE = new Singleton04();}// 公共方法public static Singleton04 getINSTANCE() {return Inner.INSTANCE;} }這個寫法非常像餓漢式寫法,單例三元素還是那三元素,只不過多加了一個內部類,將實例引用放到內部類里而已。為啥要這樣寫呢?因為 JVM 保證了內部類的線程安全,即一個內部類在整個程序中不會被重復加載,并且如果你沒有使用到內部類的話,是不會加載這個內部類的。這就非常巧妙的實現了線程安全以及節約資源的好處!
優缺點
優點:寫法簡單、節約資源(只有調用了 getINSTANCE() 方法才會加載內部類,才會實例化對象)、線程安全(JVM 保證了內部類的線程安全);
缺點:會被序列化或者反射破壞單例。
這個缺點可以說是吹毛求疵,因為之前所有寫法都會被序列化、反射破壞單例。雖然說是吹毛求疵,但咱們搞技術的還是得做到了解全部細節,我來演示一下怎樣破壞這個單例。
通過反射破壞單例
public static void main(String[] args) throws Exception {// 創建100個線程同時訪問實例for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println(Singleton04.getINSTANCE().hashCode());}).start();}// 反射破壞單例Class<Singleton04> clazz = Singleton04.class;// 拿到無參構造函數并將其設置為可訪問,無視privateConstructor<Singleton04> constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);// 創建對象Singleton04 singleton04 = constructor.newInstance();System.out.println("反射:" + singleton04.hashCode()); }運行結果如下:
... 2115147268 2115147268 反射:1078694789 2115147268 2115147268 ...如果是通過正常的訪問實例方法,是完全可以做到單例的要求。但是如果用反射的形式來創建一個對象,則就破壞了單例,一個程序中就出現了多個不同的實例對象。那么為了解決這個吹毛求疵的問題,聰明的前輩們想到了一個完美的寫法!
枚舉
// 注意,這里是枚舉 public enum Singleton05 {// 實例INSTANCE;// 公共方法public static Singleton05 getINSTANCE() {return INSTANCE;} }哎嘿,不是說所有單例都是那三元素嗎,這里怎么只有兩個元素呀!這是因為枚舉就沒有構造方法,自然而然就做到了私有化構造函數的效果,而且比私有化構造函數效果更好!因為都沒有構造函數了,連序列化和反射都破壞不了這種寫法的單例!
眼見為實,我們做個試驗:
public static void main(String[] args) throws Exception {// 創建100個線程同時訪問實例for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println(Singleton05.getINSTANCE().hashCode());}).start();}// 反射破壞單例Class<Singleton05> clazz = Singleton05.class;// 拿到無參構造函數并將其設置為可訪問,無視privateConstructor<Singleton05> constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);// 創建對象Singleton05 singleton05 = constructor.newInstance();System.out.println("反射:" + singleton05.hashCode()); }運行結果如下:
... 422057313 422057313 422057313 422057313Exception in thread "main" java.lang.NoSuchMethodException: Singleton05.<init>()at java.lang.Class.getConstructor0(Class.java:3082)at java.lang.Class.getDeclaredConstructor(Class.java:2178)當運行到反射那一塊代碼的時候,程序直接報錯,原因就是我之前所說的一樣,枚舉沒有構造方法,你自然就無法通過反射來創建對象了!
優缺點
此方法乃是最完美的方法,真是佩服想出這種寫法的前輩!
總結
五個寫法全部介紹完畢,每個寫法都有其特點,根據自己的需求來寫就好了!每種寫法理解其特點后,寫出來也就非常輕松。就像我一開始說的一樣,理解這五種寫法也不是吊書袋,每一種寫法都有其背后的思考,有些寫法思路真的讓人嘆服,至少我了解到內部類和枚舉寫法的時候我心里就是:我靠!這都能想出來,太牛逼了吧......
好的代碼就是藝術作品,希望我們都能碼出好的藝術出來!
最后,再附上我歷時三個月總結的?Java 面試 + Java 后端技術學習指南,筆者這幾年及春招的總結,github 1.5k star,拿去不謝! 下載方式1.?首先掃描下方二維碼2.?后臺回復「Java面試」即可獲取 《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的回字有四种写法,那你知道单例有五种写法吗的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 卧槽!阿里云推出“网盘”,百度网盘迎来劲
- 下一篇: 全网最全程序员效率工具及小技巧