女朋友驚掉下巴問我:單例模式竟有七種寫法?
前言
接下來,我們要進入的是設計模式篇,關于設計模式,作為程序員的你,肯定在工作中或者面試中遇到過很多次了吧
記得當時18年上大三的時候出去找實習,也問過了解哪些設計模式,不過我個人回答的最多的最詳細的大概也就是單例模式了,因為我覺得這個應該是最最好理解的了,雖然有很多種寫法,這是為了解決不同環境下的不同問題,當時我應該是把懶漢、餓漢直接都手撕了一遍,也簡單的把懶漢和餓漢的區別說了說
當時令我吃驚的是面試官告訴我,單例模式其實有七種寫法,甚至可以更多,我當時驚得下巴都掉了,當時我就感覺到了這個行業滿滿的挑戰和滿滿的知識等著我學習
果不其然,現在越學越覺得自己廢物,越學越感覺自己有太多不會的了,不過這個路肯定還是要走下去的,撥開云霧見天明,堅持下去吧
接下來我們來簡單介紹下單例模式
單例模式,顧名思義,就是唯一的實例。在當前進程中,有且只有一個單例模式創建的類對象
比如生活中的太陽、只能有一個吧,所以只能有一個實例,這個例子要是用在當年后羿射箭之前不合適,但是現在應該還算是合適的吧
再比如寫一個校園管理系統,有一個校長的角色,只能有一個,這個對象在該系統中做成單例就比較合適(其余的是副校長的 親
這個模式應該是大家最常見的,也是大家認為最簡單的了吧,但是實際上這個模式里面還是有很多細節的,也有很多的點值得大家思考的,待會咱們一起看各種寫法的時候大家記得帶著你的思考和你的問題去學習
正文
單例模式特點
單例模式有如下的特點:
1、一個JVM中有且只有一個實例的存在,構造器私有,外部無法創建該實例
2、提供一個公開的get方法獲得唯一的這個實例
有哪些優點呢:
1、省去了new的操作,降低系統內存的使用頻率,減輕GC的壓力
2、系統中的一些類需要全局單例,比如spring中的controller,再比如人類的太陽
3、避免了資源的重復的占用,減少了內存的開銷
其實也是有一些缺點的:
沒有接口,不可繼承與單一職責原則沖突,一個類應該只關心內部邏輯,而不關心外面怎么樣來實例化
先把要介紹的七種給大家說一下,大家有個印象
餓漢式、懶漢式線程不安全和安全版、DCL雙重檢測鎖模式的線程不安全和安全版、靜態內部類、枚舉類
大家先聽個耳熟,下面一一介紹
餓漢式
餓漢式,就是比較餓,于是乎吃的比較早,也就是創建的比較早,會隨著JVM的啟動而初始化該單例
也正是由于這種類裝載的時候就完成了單例的實例化了,不存在所謂的線程安全問題,是線程安全的,相應的缺點就是未達到lazy loading的效果,如果創建的這個單例類始終未用到,便回造成資源浪費
其實在實際開發中,即使知道一定用得到,我們一般也不太會使用這種機制,因為如果單例對象很多,會影響啟動的速度,采用懶加載機制是比較節約資源的
開發中很多思想也是采用懶加載,只有當真正用到一個東西的時候才允許它占用相應的資源
- /**
- * 餓漢式:通過classloader機制避免了多線程的同步問題,在類裝載的時候完成實例化
- * 優點:寫法簡單,類裝載的時候完成實例化,避免了線程同步的問題
- * 缺點:未達到lazy loading的效果,如果始終未用到則可能造成資源浪費
- * 適用場景:
- */
- public class HungrySingleton {
- //1、構造器私有化
- private HungrySingleton(){}
- //2、類的內部創建對象的實例
- private final static HungrySingleton dayu = new HungrySingleton();
- //3、將類的內部實例提供一個靜態方法返回出去
- private static HungrySingleton getInstance(){
- return dayu;
- }
- }
懶漢式(線程不安全、線程安全)
懶漢式咯,就是比較懶,在啟動的時候,不會進行該單例對象的創建,只有當真正用到的時候才會去加載這些東西
之所以加懶漢式,大概就是采用了懶加載思想
我們看下面這個懶漢式的代碼
- /**
- * 懶漢式
- * 缺點:線程不安全,工作中一般不用
- */
- public class NotSafeLazySingleton {
- //構造器私有化
- private NotSafeLazySingleton(){}
- //暫時不加載實例
- private static NotSafeLazySingleton dayu;
- /**
- * 存在線程安全問題
- * 線程A到括號dayu == null判斷完之后,進入括號內部,
- * 此時線程B獲得執行權,判斷==null也是true,所以也進入
- * 此時兩個線程便出現了兩個dayu對象
- * @return
- */
- public static NotSafeLazySingleton getInstance(){
- if(dayu == null){
- dayu = new NotSafeLazySingleton();
- }
- return dayu;
- }
- }
其實有過多線程的經驗的小伙伴應該很快就看出來了,上面這種懶漢式是有線程安全問題的,當線程A執行到if(dayu == null)這一行的時候,判斷為空,true進入括號內部,此時線程A的時間片用完了,到了線程B的執行了,于是乎也會判斷為空,進入括號內部
線程B創建了一個NotSafeLazySingleton對象,輪到線程A執行的時候,由于在之前已經判斷完進入了括號內部,于是線程A也會創建一個NotSafeLazySingleton對象
GG,這樣不是我們想要的效果,這就不屬于單例模式了,所以這種在多線程情況下是存在安全問題的
有了問題,自然就是解決咯,可能有的小伙伴也想到了,存在線程安全問題,那就加上線程安全關鍵字synchronized來解決,于是乎便有了下面的代碼,我們給函數加上關鍵字synchronized,但是這樣會造成效率極其低下
所有調用這個方法去使用單例對象的地方都需要排隊阻塞知道該鎖的釋放,在多線程情況下會迅速降低效率
- /**
- * 懶漢式安全寫法
- * 缺點:Synchronized關鍵字導致方法效率低 效率極低
- * 優點:線程安全
- * 適用場景:實際開發 不推薦使用
- */
- public class SafeLazySingleton {
- //構造器私有化
- private SafeLazySingleton(){}
- //暫時不加載實例
- private static SafeLazySingleton dayu;
- /**
- * synchronized導致所有通過該方法獲取該對象的時候都要排隊
- */
- public static synchronized SafeLazySingleton getInstance(){
- if(dayu == null){
- dayu = new SafeLazySingleton();
- }
- return dayu;
- }
- }
所有調用這個方法去使用單例對象的地方都需要排隊阻塞知道該鎖的釋放,在多線程情況下會迅速降低效率,于是有了下面的這種改進方法
只鎖其中的部分代碼,看下下面的代碼
- /**
- * 本意上是對SafeLazySingelton的改進 因為前面的對整個方法進行加鎖的效率實在是太低了
- * 但是這種還是不能起到線程同步的作用 和NotSafeLazySingelton類似 只要線程進入了== null的里面
- * 此時另一個線程獲得CPU分配的時間片 則會出現多個對象
- */
- public class NotSafeLaySingleton2 {
- //構造器私有化
- private NotSafeLaySingleton2(){}
- //暫時不加載實例
- private static NotSafeLaySingleton2 dayu;
- /**
- * @return
- */
- public static NotSafeLaySingleton2 getInstance(){
- if(dayu == null){
- synchronized (NotSafeLaySingleton2.class){
- dayu = new NotSafeLaySingleton2();
- }
- }
- return dayu;
- }
- }
上面的這種代碼看著有問題嗎?
不知道你認真讀了上面代碼之后,內心是怎么想的,聰明的小伙伴已經發現了事情不是這么簡單,發現其中了問題
是的,上面的這種改進方法,貌似實現了效率跟高些,但是會隨之帶來多線程的問題
線程A判斷dayu == null進入括號,還沒拿到NotSafeLaySingleton2的鎖,時間片消耗完了,此時線程B也判斷,發現dayu == null也成立,此時也會進入括號,假設線程B拿到了鎖,創建了一個NotSafeLaySingleton2對象,執行完之后釋放鎖。線程A拿到該鎖,會重新創建一個對象,于是出現多例現象
先是通過synchronized加在方法層面解決并發問題,但是隨之而來帶來效率問題,于是為了提高效率,加在內部,但是加在內部就有了相應的線程安全問題
說了這么多,就是要引出我們下面的線程安全的DCL的單例模式
看下怎么寫
雙重檢查鎖模式DCL- double chechked locking(線程安全)
上面那個其實屬于單重檢查鎖模式,我起的名字,因為只檢查了一個地方的鎖,正是如此也帶來了多線程的問題,于是乎就有了下面這種雙重檢測形勢的單例模式了,一起看看吧,穩得一批
- /**
- * 雙重檢測單例:穩得一批
- * 優點:線程安全 延遲加載 效率相對來說也不錯
- *使用場景:實際開發中 用的比較多
- */
- public class DoubleCheckSingleton {
- private static volatile DoubleCheckSingleton dayu;
- private DoubleCheckSingleton(){}
- /**
- * 解決線程安全的問題同時 也解決懶加載問題
- * @return
- */
- public static DoubleCheckSingleton getInstance(){
- if(dayu == null){
- synchronized (DoubleCheckSingleton.class){
- if(dayu == null){
- dayu = new DoubleCheckSingleton();
- }
- }
- }
- return dayu;
- }
- }
上面這種在進入了data == null的內部也會再次判斷一次是否還等于空,這種就很好的解決了多線程的問題
這種DCL的單例模式在工作中算是常用的一種了,有效的解決高并發下的單例模式問題
靜態內部類
靜態內部類加載單例,類加載機制保證線程安全,而且還有一個優點,懶加載,只有在調用getInstance的時候才會加載內部類,才會創建這個對象
外部類被裝載的時候,內部類不會立即被裝載,調用getInstance才會裝載,并且只會裝載一次,且不存在線程安全問題
- /**
- * 靜態內部類加載單例
- * 優點:類裝載機制保證線程安全 懶加載 只有調用getInstance才會加載內部類
- * 適用場景:
- */
- public class StaticInnerClassSingleton {
- private StaticInnerClassSingleton(){}
- /**
- * 1、外部類被裝載時 內部不會立即被裝載
- * 2、調用getInstance方法時會裝載 只會裝載一次 且不存在線程安全
- */
- private static class SingletonInstance{
- private static final StaticInnerClassSingleton dayu = new StaticInnerClassSingleton();
- }
- //返回靜態內部類中的對象
- public static StaticInnerClassSingleton getInstance(){
- return SingletonInstance.dayu;
- }
- }
枚舉類
枚舉類也是可以用作單例模式,而且還很簡單
Effective Java作者Josh Bloch所提倡的單例實現的方式就是這種,這種無線程安全問題,還可以防止反序列化重新創建新的對象
- /**
- * 枚舉實現單例
- * 優點:簡潔 無線程安全問題 還可以防止反序列化重新創建新的對象
- * Effective Java作者Josh Bloch提倡的方法
- */
- public class EnumSingleton {
- public static void main(String[] args) {
- //instance和instance2是同一個對象
- Singleton instance = Singleton.INSTANCE;
- Singleton instance2 = Singleton.INSTANCE;
- }
- enum Singleton{
- INSTANCE;
- }
- }
總結
設計模式應該屬于面試高頻,而單例模式又是設計模式的最簡單,或者說是最常見的設計模式之一,看完這篇文章,大家應該都知道單例模式的多種寫法了,也知道各種的優劣勢和相應的使用場景了
我們思考一個問題,為什么要使用單例模式而使用靜態方法
這兩個其實都可以實現我們加載的最終目的,但是他們一個是基于對象的,一個是屬于面向對象的,就像是很多種情況,我們通過普通的編碼也可以實現,但是我們引入設計模式來更好的體現編程思想
如果一個方法和他所在的類的實例對象確實是無關的,那么它就應該是靜態的,反之它就應該是非靜態的,如果我們需要使用非靜態的方法,但是在創建類對象的時候,又只需要維護一個實例,不想創建多個不同的實例,就需要使用單例模式了。