Java設計模式之單例模式

来源:http://www.cnblogs.com/qifengshi/archive/2017/03/14/6549156.html
-Advertisement-
Play Games

單例模式是非常常見的設計模式,其含義也很簡單,一個類給外部提供一個唯一的實例。下文所有的代碼均在 "github" 源碼整個項目不僅僅有設計模式,還有其他JavaSE知識點,歡迎Star,Fork 單例模式的UML圖 單例模式的關鍵點 通過上面的UML圖,我們可以看出單例模式的特點如下: 1. 構造 ...


單例模式是非常常見的設計模式,其含義也很簡單,一個類給外部提供一個唯一的實例。下文所有的代碼均在github
源碼整個項目不僅僅有設計模式,還有其他JavaSE知識點,歡迎Star,Fork

單例模式的UML圖

單例模式的UML圖

單例模式的關鍵點

通過上面的UML圖,我們可以看出單例模式的特點如下:

  1. 構造器是私有的,不允許外部的類調用構造器
  2. 提供一個供外部訪問的方法,該方法返回單例類的實例

如何實現單例模式

上面已經給出了單例模式的關鍵點,我們的實現只需要滿足上面2點即可。但是正因為單例模式的實現方式比較寬鬆,所以不同的實現方式會有不同的問題。我們可以對單例模式的實現做一下分類,看一看有哪些不同的實現方式。

  1. 根據單例對象的創建時機不同,可以分為餓漢模式和懶漢模式。餓漢是指在類載入的時候,就創建了對象。但是創建對象有時比較消耗資源,會造成類載入很慢,但是優點是獲取對象的速度很快,因為早已經創建好了嘛。懶漢就是相對餓漢而言,在需要返回單例對象的時候,在創建對象,類載入的時候,並不初始化,好處與缺點也不言而喻
  2. 根據是否實現線程安全,可以分為普通的懶漢模式這種線程不安全的寫法,和餓漢模式,雙重檢查鎖的懶漢模式,以及通過靜態內部類或者枚舉類等實現的線程安全的寫法。

一個線程不安全的單例模式

public class SimpleSingleton {

    private static SimpleSingleton simpleSingleton;

    private SimpleSingleton(){

    }

    public static SimpleSingleton getInstance(){
        if (simpleSingleton == null) {
            simpleSingleton = new SimpleSingleton();
        }
        return simpleSingleton;
    }
}

首先,我們可以看出這是一個懶漢模式的實現。因為只有在getInstance的時候,才會真正創建單例的對象。但是為什麼他是線程不安全的呢,是因為可能會有2個線程同時進入if (simpleSingleton == null)的判斷,就是同時創建了simpleSingleton對象。

DCL懶漢模式

上面的方法可以看出是存線上程不安全的問題的,我們可以用同步關鍵字synchronized來實現線程安全。我們先逐步分析,先用synchronized來改寫上面的懶漢模式,代碼如下:

public class DCLSingleton {

    private static DCLSingleton singleton;
    private DCLSingleton(){
    }

    public synchronized static DClSingleton getSingleton(){
        if (singleton == null) {
            singleton = new DCLSingleton();
        }
        return singleton;
    }

}

這樣,就有效的保證了不會有兩個線程同時執行該方法,但這個效率也太低了吧。因為在創建實例之後,每次得到實例對象,還是需要進行同步,synchronized的同步保證代價是比較大的,因此可以在此基礎上進行改造。在已經創建好之後,就不需要同步了,我們可以改成如下的形式:

    public  static DCLSingleton getSingleton(){
        if (singleton == null) {
            synchronized (DCLSingleton.class) {
                if (singleton == null) {
                    singleton = new DCLSingleton();
                }
            }
        }
        return singleton;
    }

其他代碼不變,只看這個方法。該方法的兩重if (singleton == null)可以有效地保證線程安全。比如,當兩個線程同時進入該方法的時候,第一個if,兩者都是進入,下麵的代碼,但是碰到同步代碼塊,只能有一個先進入,進入的時候,繼續判斷,再次判斷為空,才會真正創建對象。如果不進行,第二個判斷,那些對於第一個進入的線程而言,確實創建了對象,但是第二個線程,他緊接著也會執行創建對象的操作,因為不知道第一個線程已經創建成功。因此,需要兩次判空。
但是真的就如此簡單的保證了線程安全嗎?我們仔細分析一下這個過程,singleton = new DCLSingleton();這個代碼實際上是3個操作。

  1. 給DCLSingleton實例分配記憶體
  2. 調用DCLSingleton()的構造函數,初始化成員欄位
  3. 將singleton對象指向分配的記憶體空間。

在JDK1.5以前,上面的3個執行順序是不固定的,有可能是1-2-3,或者1-3-2。如果是1-3-2,則在第一個線程執行完第三步以後,第二個線程立即執行,但還沒有真正的進行初始化,所以就會使用的時候出錯。在JDK1.5以後,我們可以用volatile關鍵字來保證該1-2-3的順序執行。所以,除了getSingleton()方法要改成上面的樣子以外,還需要對private static DCLSingleton singleton; 改寫成private static volatile DCLSingleton singleton; 這樣,就真正保證了線程同步的懶漢寫法的單例模式。

餓漢寫法

餓漢寫法有很多變形,但無論是哪一種變形,都能保證線程安全,因為餓漢寫法是在類載入的時候,就完成了對象的初始化,類載入保證了他們天生是線程安全的。下麵給出常見的2中餓漢寫法

public class HungrySingleton {
    private static final HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton(){

    }

    public static HungrySingleton getSingleton(){
        return singleton;
    }
}
public class HungrySingleton {
    private static final HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton(){

    }

//    public static HungrySingleton getSingleton(){
//        return singleton;
//    }
}

這兩種對初始化單例的對象上面,都是一致的, 通過final來保證對象的唯一。不同的是,調用單例對象的方式,第一種是通過getSingleton(),第二種是通過類.類變數的形式。

靜態內部類實現單例模式

雙重檢查鎖(DCL)實現單例模式,雖然解決了線程不安全的問題,以及保證了資源的懶載入,在需要的時候,才會進行實例化的操作。但是在某些情況下(比如JDK低於1.5)會出現DCL失效,所以有一種很簡潔且依舊是懶載入的方法實現單例模式。寫法如下:

public class StaticSingleton {

    private StaticSingleton(){
    }
    public static final StaticSingleton getInstance(){
        return Holder.singleton;
    }

    private static class Holder{
        private static final StaticSingleton singleton = new StaticSingleton();
    }
}

通過靜態內部類的形式,實現單例類的初始化,其特性同樣是通過ClassLoader來保證其單例對象的唯一,但是這是懶載入的,因為只有在Holder類被調用的時候,即getInstance調用的時候,才會載入Holder類從而實現創建對象。

枚舉類實現單例模式

直接看代碼:

public enum EnumSingleton {
    SINGLETON;
    public void doSometings(){
        
    }
}

使用的時候,直接通過EnumSingleton.SINGLETON.doSomethings()。枚舉類天生特性是保證不會有兩個實例,並且只有在第一次訪問的時候才會被實例化,是懶載入的情況。

真的不會再次創建新的對象嗎?

在常規調用單例類的getInstance()方法的情況下,使用線程安全的寫法確實不會創建新的對象,但是Java提供了很多奇特的技巧和使用,下麵這些使用會破壞掉常規的單例。

  1. 反序列化
  2. 反射
  3. 克隆
  4. 分散式環境下,多個類載入器

在除了枚舉實現單例模式的方法以外,其餘所有方法碰到上述四種情況,都會重新創建對象。原因如下:

  1. 反序列化會調用一個特殊的readResolve()方法來創建新的對象。我們可以重寫該方法,讓他返回原來的instance,而不是重新創建一個。
  2. 反射會得到私有的構造函數,只能在構造函數中加一個判斷,如果對象不為null,則扔出一個運行時異常,如果不這樣,只有枚舉能解決,因為枚舉自帶的特性。
  3. 克隆,因為直接拷貝的記憶體空間的內容,所以只有自己重寫單例類的clone方法,如果不這樣,也只有枚舉能解決,因為枚舉沒有克隆方法。
  4. 多分散式環境,因為我們上述很多種單例的寫法,都是依賴於類載入器的特性,但是static的作用只負責到類載入器,所以當工程中存在多個類載入器的時候,就會創建多個實例,這種通常就需要第三方庫來解決。

什麼時候用單例模式,用哪一種寫法的單例模式

單例模式有兩種比較適合的使用場景。
第一種是創建某個對象,需要的代價比較大,為了避免頻繁的創建和銷毀對象從而引起的對資源的浪費,會考慮使用單例模式。
第二種是這個對象必須只有一個,有多個會造成不可預估的錯誤,或者程式的混亂,比如只會有一個序號生成器,一個緩存等等。
針對使用的單例模式,如果需要理解的載入資源,就是用餓漢寫法,在Android應用中,很多對象需要在啟動的時候,立即就使用,比如啟動時,需要拉取相機配置的類管理縮略圖的cache類等等。如果不是立即需要,或者不是貫穿應用始終的,就不需要使用餓漢寫法,可以考慮懶漢寫法用(DCL或者靜態內部類實現)這兩種在一般情況下都不會出現問題。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 先說原理:推送是建立在所有蘋果設備在聯網狀態下都會跟蘋果伺服器進行一個長連接的,長連接的概念是相對於短連接的,長連接可以向客戶端發送消息,保證了數據的及時性。藉助蘋果設備與蘋果的APNS伺服器之間的長連接,通過APNS伺服器將消息發送給客戶端。首先蘋果應用要註冊通知,然後將設備的UDID和應用的bo ...
  • 繼續做仿造著別人的第二個 1.首先下載 一些字體 網上搜索 "造字工房" 2.把下載的相應字體文件放到工程之中,就Ok了 不多說 效果如下 可以下麵這個方法 檢索項目裡面所有的字體 代碼如下 ...
  • 環境: react native: 0.41.2 react native image picker: 0.26.2 xcode 8.2.1 iphone 6 根據官方教程( "https://github.com/marcshilling/react native image picker" )寫 ...
  • 最近公司項目不是很忙,偶然間看到編程語言排行榜,看到swift 已經排到前10了,然OC排名也越來越後了,感覺要上車了,雖然現在項目都是用OC寫的,但是swift是一種趨勢。在網上看到“自學 iOS - 三十天三十個 Swift 項目” 這篇博客,我也想自己在閑暇之餘學習下swift,在看了2天的s ...
  • 1.如何使edittext點擊後全選裡面的內容啊? 2.記一次EditText設置預設選中setSelection的一個bug 需求:在重命名的時候,要預設選中文件的尾碼之前的名字; 代碼如下: 但是發現有的文件重命名的時候app崩潰,報錯如下:java.lang.IndexOutOfBoundsE ...
  • Environment.getExternalStorageDirectory()是Android 2.x時代的產物,那時Android主流設備只有很小的內置存儲器,然後都會外置一張sd卡,那時這個方法返回的就是外置sd卡的根路徑。但隨著Android進入4.x時代,大部分Android設備都已經內 ...
  • 一、簡介 Vitamio能夠流暢播放720P甚至1080P高清MKV,FLV,MP4,MOV,TS,RMVB等常見格式的視頻,還可以在Android 與 iOS 上跨平臺支持 MMS, RTSP, RTMP, HLS(m3u8) 等常見的多種視頻流媒體協議,包括點播與直播。 使用Vitamio框架主 ...
  • 1. 使用流程 2. 常用屬性   numColumns:指定顯示的列數,若不指定該屬性,則預設所有項排成1列。   verticalSpacing:行距。   horizontalSpacing:列距,若指定了numColumns,則按照 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...