成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

在Java中如何寫一個正確的單例模式

開發 前端
如果是在面試中遇到這個問題,那么你可以從一開始的餓漢式、懶漢式說起,一步步分析每種寫法的優缺點,并對寫法進行演進,然后重點講一下雙重檢查模式為什么需要兩次檢查,以及為什么需要 volatile 關鍵字,最后再說到枚舉類寫法的優點和背后的原理,相信這一定會為你的面試加分。

今天我們一起來探討下單例模式,可以說,單例模式是面試常客,如果考察你對設計模式的理解程度,那么有很大可能會考察到,因為單例模式雖然看似簡單,每個人都可能寫出來。但如果往深了挖,又能考察出面試候選人對于并發、類加載、序列化等重要知識點的掌握程度和水平。單例模式有很多種寫法,那么哪種寫法更好呢,為什么?

要想知道哪種寫法好,首先我們需要知道什么是單例模式,單例模式指的是,保證一個類只有一個實例,并且提供一個全局可以訪問的入口。

舉個例子,這就好比是“分身術”,但是每個“分身”其實都對應同一個“真身”。

那么我們為什么需要單例呢,其中一個理由,那就是為了節省內存、節省計算。很多情況下,我們只需要一個實例就夠了,如果出現了更多的實例,反而屬于浪費。舉個例子,我們就拿一個初始化比較耗時的類來說:

public class ExpensiveResource {
    public ExpensiveResource() {
        field1 = // 查詢數據庫
        field2 = // 然后對查到的數據做大量計算
        field3 = // 加密、壓縮等耗時操作
    }
}

這個類在構造的時候,需要查詢數據庫并對查到的數據做大量計算,所以在第一次構造時,我們花了很多時間來初始化這個對象。但是假設我們數據庫里的數據是不變的,并把這個對象保存在了內存中,那么以后就用同一個實例了,如果每次都重新生成新的實例,實在是沒必要。

接下來看看需要單例的第二個理由,那就是為了保證結果的正確。比如我們需要一個全局的計數器,用來統計人數,那么如果有多個實例,反而會造成混亂。

另外呢,就是為了方便管理。很多工具類,我們只需要一個實例,那么我們通過統一的入口,比如通過 getInstance 方法去獲取這個單例是很方便的,太多實例不但沒有幫助,反而會讓人眼花繚亂。

在了解了單例模式的好處之后,我們接下來就來探討一下單例模式有哪些適用場景。

無狀態的工具類:比如日志工具類,不管是在哪里使用,我們需要的只是它幫我們記錄日志信息,除此之外,并不需要在它的實例對象上存儲任何狀態,這時候我們就只需要一個實例對象。

全局信息類:比如我們在一個類上記錄網站的訪問次數,并且不希望有的訪問被記錄在對象 A 上,有的卻被記錄在對象 B 上,這時候我們就可以讓這個類成為單例,需要計數的時候拿出來用即可。

常見的寫法又有哪些呢,我認為有這么 5 種:餓漢式、懶漢式、雙重檢查式、靜態內部類式、枚舉式。

我們按照寫法的難易度來逐層遞講,先來看下相對簡單的餓漢式寫法具體是什么樣的。

public class Singleton {
    private static Singleton singleton = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return singleton;
    }
}

用 static 修飾我們的實例,并把構造函數用 private 修飾。這是最直觀的寫法。由 JVM 的類加載機制保證了線程安全。

這種寫法的缺點也比較明顯,那就是在類被加載時便會把實例生成出來,所以假設我們最終沒有使用到這個實例的話,便會造成不必要的開銷。

下面我們再來看下餓漢式的變種——靜態代碼塊形式。

public class Singleton {
    private static Singleton singleton;
    static {
        singleton = new Singleton();
    }
    private Singleton() {}
    public static Singleton getInstance() {
        return singleton;
    }
}

這種寫法把新建對象的相關代碼轉移到了靜態代碼塊里,在原理上和上面那一種“餓漢式”的寫法是比較相近的,所以同樣會在類加載的時候完成實例的創建。

在了解了餓漢式的寫法后,再來看下第二種寫法,懶漢式。

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

這種寫法的優點在于,只有在 getInstance 方法被調用的時候,才會去進行實例化,所以不會造成資源浪費,但是在創建的過程中,并沒有考慮到線程安全問題,如果有兩個線程同時執行 getInstance 方法,就可能會創建多個實例。所以這里需要注意,不能使用這種方式,這是錯誤的寫法。

為了避免發生線程安全問題,我們可以對前面的寫法進行升級,那么線程安全的懶漢式的寫法是怎樣的呢。

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

我們在 getInstance 方法上加了 synchronized 關鍵字,保證同一時刻最多只有一個線程能執行該方法,這樣就解決了線程安全問題。但是這種寫法的缺點也很明顯:如果有多個線程同時獲取實例,那他們不得不進行排隊,多個線程不能同時訪問,然而這在大多數情況下是沒有必要的。

為了提高效率,縮小同步范圍,就把 synchronized 關鍵字從方法上移除了,然后再把 synchronized 關鍵字放到了我們的方法內部,采用代碼塊的形式來保護線程安全。

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

這種寫法是錯誤的。它的本意是想縮小同步的范圍,但是從實際效果來看反而得不償失。因為假設有多個線程同時通過了 if 判斷,那么依然會產生多個實例,這就破壞了單例模式。

所以,為了解決這個問題,在這基礎上就有了“雙重檢查模式”。

public class Singleton {
    privatestaticvolatile Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton =  new Singleton();
                }
            }
        }
        return singleton;
    }
}

這種寫法的優點就是不僅做到了延遲加載,而且是線程安全的,同時也避免了過多的同步環節。我們重點來看一下 getInstance 方法,這里面有兩層 if 判空,下面我們分別來看一下每個 if 的作用。

這里涉及到一個常見的問題,面試官可能會問你,“為什么要 double-check?去掉第二次的 check 行不行?”這時你需要考慮這樣一種情況,有兩個線程同時調用 getInstance 方法,并且由于 singleton 是空的,所以兩個線程都可以通過第一個 if。

然后就遇到了 synchronized 鎖的保護,假設線程 1 先搶到鎖,并進入了第二個 if,那么線程 1 就會創建新實例,然后退出 synchronized 代碼塊。接著才會輪到線程 2 進入 synchronized 代碼塊,并進入第二層 if,此時線程 2 會發現 singleton 已經不為 null,所以直接退出 synchronized 代碼塊,這樣就保證了沒有創建多個實例。假設沒有第二層 if,那么線程 2 也可能會創建一個新實例,這樣就破壞了單例,所以第二層 if 肯定是需要的。

而對于第一個 check 而言,如果去掉它,那么所有線程都只能串行執行,效率低下,所以兩個 check 都是需要保留。

相信你可能看到了,我們在雙重檢查模式中,給 singleton 這個對象加了 volatile 關鍵字,那 為什么要用 volatile 呢?這是因為 new 一個對象的過程,其實并不是原子的,至少包括以下這 3 個步驟:

  1. 給 singleton 對象分配內存空間;
  2. 調用 Singleton 的構造函數等,來進行初始化;
  3. 把 singleton 對象指向在第一步中分配的內存空間,而在執行完這步之后,singleton 對象就不再是 null 了。

這里需要留意一下這 3 個步驟的順序,因為存在重排序,所以上面所說的三個步驟的順序,并不是固定的。雖然看起來是 1-2-3 的順序,但是在實際執行時,也可能發生 1-3-2 的情況,也就是說,先把 singleton 對象指向在第一步中分配的內存空間,再調用 Singleton 的構造函數。

如果發生了 1-3-2 的情況,線程 1 首先執行新建實例的第一步,也就是分配單例對象的內存空間,然后線程 1 因為被重排序,所以去執行了新建實例的第三步,也就是把 singleton 指向之前的內存地址,在這之后對象不是 null,可是這時第 2 步并沒有執行。假設這時線程 2 進入 getInstance 方法,由于這時 singleton 已經不是 null 了,所以會通過第一重檢查并直接返回 singleton 對象并使用,但其實這時的 singleton 并沒有完成初始化,所以使用這個實例的時候會報錯。

最后,線程 1“姍姍來遲”,才開始執行新建實例的第二步——初始化對象,可是這時的初始化已經晚了,因為前面已經報錯了。

到這里關于“為什么要用 volatile”問題就講完了,使用 volatile 的意義,我認為主要在于呢,它可以防止剛講到的重排序的發生,也就避免了拿到沒完成初始化的對象。

接下來要講到的這種方式,靜態內部類的寫法,利用了類裝載時由 JVM 所保證的單線程原則,進而保證了線程安全。

public class Singleton {
    private Singleton() {}
    private static class SingletonInstance {
        private static final Singleton singleton = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonInstance.singleton;
    }
}

相比于餓漢式在類加載時就完成實例化,這種靜態內部類的寫法并不會有這個問題,這種寫法只有在調用 getInstance 方法時,才會進一步完成內部類的 singleton 的實例化,所以不存在內存浪費的問題。

這里簡單做個小總結,靜態內部類寫法與雙重檢查模式的優點一樣,都是避免了線程不安全的問題,并且延遲加載,效率高。

可以看出,靜態內部類和雙重檢查的寫法都是不錯的寫法,但是它們不能防止被反序列化生成多個實例,那有沒有更好的寫法呢?最后我們來看枚舉方式的寫法。

public enum Singleton {
    INSTANCE;
    public void myMethod() {
    }
}

這就是枚舉方式的寫法,下面我們會對這種寫法進行展開分析。

前面我們講了餓漢式、懶漢式、雙重檢查、靜態內部類、枚舉這 5 種寫法,有了這么多方法可以實現單例,這時你可能會問了,那我該怎么選擇,用哪種單例的實現方案最好呢?

Joshua Bloch(約書亞·布洛克)在《Effective Java》一書中明確表達過一個觀點:“使用枚舉實現單例的方法,雖然還沒有被廣泛采用,但是單元素的枚舉類型已經成為了實現 Singleton 的最佳方法。”

為什么他會更為推崇枚舉模式的單例呢?這就不得不回到枚舉寫法的優點上來說了,枚舉寫法的優點有這么幾個:

首先就是寫法簡單。枚舉的寫法不需要我們自己考慮懶加載、線程安全等問題。同時,代碼也比較“短小精悍”,比任何其他的寫法都更簡潔,很優雅。

第二個優點是線程安全有保障,枚舉類的本質也是一個 Java 類,但是它的枚舉值會在枚舉類被加載時完成初始化,所以依然是由 JVM 幫我們保證了線程安全。

前面幾種實現單例的方式,其實是存在隱患的,那就是可能被反序列化生成新對象,產生多個實例,從而破壞了單例模式。接下來要說的枚舉寫法的第 3 個優點,它恰恰解決了這些問題。

對 Java 官方文檔中的相關規定翻譯如下:“枚舉常量的序列化方式不同于普通的可序列化或可外部化對象。枚舉常量的序列化形式僅由其名稱組成;該常量的字段值不存在于表單中。要序列化枚舉常量,ObjectOutputStream 將寫入枚舉常量的 name 方法返回的值。要反序列化枚舉常量,ObjectInputStream 從流中讀取常量名稱;然后,通過調用 java.lang.Enum.valueOf 方法獲得反序列化常量,并將常量的枚舉類型和收到的常量名稱作為參數傳遞。”

也就是說,對于枚舉類而言,反序列化的時候,會根據名字來找到對應的枚舉對象,而不是創建新的對象,所以這就防止了反序列化導致的單例破壞問題的出現。

對于通過反射破壞單例而言,枚舉類同樣有防御措施。反射在通過 newInstance 創建對象時,會檢查這個類是否是枚舉類,如果是,就拋出 IllegalArgumentException(“Cannot reflectively create enum objects”) 異常,反射創建對象失敗。

可以看出,枚舉這種方式,能夠防止序列化和反射破壞單例,在這一點上,與其他的實現方式比,有很大的優勢。安全問題不容小視,一旦生成了多個實例,單例模式就徹底沒用了。

所以結合講到的這 3 個優點,寫法簡單、線程安全、防止反序列化和反射破壞單例,枚舉寫法最終勝出。

今天的分享到這里就結束了,最后我來總結一下。今天我講解了單例模式什么是,它的作用、用途,以及 5 種經典寫法,其中包含了餓漢式、懶漢式、雙重檢查方式、靜態內部類方式和枚舉的方式,最后我們還經過對比,看到枚舉方式在寫法、線程安全,以及避免序列化、反射攻擊上,都有優勢。

這里也跟大家強調一下,如果使用線程不安全的錯誤的寫法,在并發情況下可能產生多個實例,那么不僅會影響性能,更可能造成數據錯誤等嚴重后果。

如果是在面試中遇到這個問題,那么你可以從一開始的餓漢式、懶漢式說起,一步步分析每種寫法的優缺點,并對寫法進行演進,然后重點講一下雙重檢查模式為什么需要兩次檢查,以及為什么需要 volatile 關鍵字,最后再說到枚舉類寫法的優點和背后的原理,相信這一定會為你的面試加分。

另外在工作中,要是遇到了全局信息類、無狀態工具類等場景的時候,推薦使用枚舉的寫法來實現單例模式。

責任編輯:武曉燕 來源: 程序員技術充電站
相關推薦

2019-08-01 12:59:21

Bug代碼程序

2022-10-08 00:06:00

JS運行V8

2024-02-22 10:02:03

單例模式系統代碼

2021-09-07 10:44:35

異步單例模式

2021-02-07 23:58:10

單例模式對象

2017-02-06 10:30:13

iOS表單正確姿勢

2011-09-08 10:46:12

Widget

2015-04-29 10:02:45

框架如何寫框架框架步驟

2011-03-16 10:13:31

java單例模式

2011-06-10 15:21:25

Qt 控制臺

2011-06-28 15:18:45

Qt 單例模式

2021-05-29 10:22:49

單例模式版本

2024-02-04 12:04:17

2013-03-26 10:35:47

Objective-C單例實現

2021-03-15 07:02:02

java線程安全

2021-02-01 10:01:58

設計模式 Java單例模式

2021-03-02 08:50:31

設計單例模式

2017-08-21 16:36:12

語法樹AST解析器HTML5

2022-02-06 22:30:36

前端設計模式

2017-09-18 09:03:36

線程安全單例
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲区中文字幕 | 国产成人免费视频网站高清观看视频 | 国产精品视频导航 | 精品久久久久久一区二区 | 成人免费视频网站在线看 | 中文字幕成人免费视频 | 精品国产网 | 国产一区二区三区在线免费观看 | 精品国产乱码久久久久久丨区2区 | 欧美日韩在线视频一区 | 亚洲久久在线 | 99精品欧美一区二区三区 | 人人做人人澡人人爽欧美 | 日日噜噜夜夜爽爽狠狠 | 成年免费大片黄在线观看岛国 | 中日韩av | 日韩免费毛片 | 亚洲高清在线 | 成人精品一区二区三区中文字幕 | 天天操夜夜爽 | 亚洲精品日韩在线 | 国产一区二区久久 | 精品一区二区三区四区 | 亚洲免费高清 | 国产精品自产拍 | 国产 欧美 日韩 一区 | 国产在线精品一区二区三区 | 亚洲毛片 | 久久精品亚洲精品国产欧美 | 在线播放国产一区二区三区 | 黄色在线免费观看视频网站 | av在线视 | 日韩精品中文字幕一区二区三区 | 久久久久亚洲精品 | 国产乱码精品一区二区三区中文 | 久久亚洲一区二区 | 久久久久久高清 | 国产高清视频一区 | 黄色大全免费看 | 天堂色区 | 精精国产xxxx视频在线 |