Java設計模式-3.單例模式概述與使用

  Java設計模式-單例模式概述與使用

參考:https://blog.csdn.net/fly910905/article/details/79286680

1、單例模式的概述與使用

  • 單例模式,就是要確保類在內存中只有一個對象,該對象必須自動創建,並且對外提供。
  • 優點:系統內存中只存在一個對象,因此可以節約系統資源,對於一些需要頻繁創建和銷燬對象的方法,單例模式無疑可以提高系統的性能。
  • 缺點:沒有抽象層,因此擴展很難。職責過重,在一定程序上違背了單一職責。
  • 單例模式的分類:
             單例模式之餓漢式:類一加載的時候就創建對象。
             單例模式之懶漢式:類一加載的時候並不急着去創建對象,而是在需要用的時候纔去創建對象。

2、單例模式之餓漢式:開發時使用

(0)單例模式的實現步驟:3部曲

  • 1)把類的構造方法私有化
  • 2)在類中的成員位置自己創建一個供別人使用的唯一的類對象
    • 由於靜態方法只能訪問靜態成員變量,因此這個類對象需要加靜態static修飾
    • 又因爲不能夠讓外界直接訪問修改這個類對象的值,因此需要加私有修飾符private修飾
  • 3)在類中提供一個對外公共的訪問方式,爲了讓外界能夠直接使用該公共訪問方式,該方式需要加靜態static修飾

(1)實現單例模式的學生類

package cn.itcast_03;
public class Student {
	private Student() { }
	private static Student s = new Student();
	public static Student getStudent() {
		return s;
	}
}

(2)測試該單例模式

package cn.itcast_03;

/*
 * 單例模式:保證類在內存中只有一個對象。
 * 如何保證類在內存中只有一個對象呢?
 * 		A:把構造方法私有
 * 		B:在成員位置自己創建一個對象
 * 		C:通過一個公共的方法提供訪問
 */
public class StudentDemo {
	public static void main(String[] args) {
		// Student s1 = new Student();
		// Student s2 = new Student();
		// System.out.println(s1 == s2); // false
		Student s1 = Student.getStudent();
		Student s2 = Student.getStudent();
		System.out.println(s1 == s2);
		System.out.println(s1); // null,cn.itcast_03.Student@175078b
		System.out.println(s2);// null,cn.itcast_03.Student@175078b
	}
}

3、單例模式之懶漢式:面試時使用

(1)實現單例模式的老師類

package cn.itcast_03;
public class Teacher {
	private Teacher() {  }
	private static Teacher t = null;
	public synchronized static Teacher getTeacher() {
		// 假設有三個線程 t1,t2,t3
		if (t == null) {
		    //t1,t2,t3
		    t = new Teacher();
		}
		return t;
	}
}

(2)測試該單例模式

package cn.itcast_03;
public class TeacherDemo {
	public static void main(String[] args) {
		Teacher t1 = Teacher.getTeacher();
		Teacher t2 = Teacher.getTeacher();
		System.out.println(t1 == t2);
		System.out.println(t1); // cn.itcast_03.Teacher@175078b
		System.out.println(t2);// cn.itcast_03.Teacher@175078b
	}
}

4、單例模式的面試題

 * 單例模式:
 *         餓漢式:類一加載就創建對象
 *         懶漢式:用的時候,纔去創建對象
 * 面試題:單例模式的思想是什麼?請寫一個代碼體現?
 *         開發:餓漢式(是不會出問題的單例模式)
 *         面試:懶漢式(可能會出問題的單例模式)
 *                   A:懶加載(延遲加載)  :不會出問題 
 *                   B:線程安全問題:可能會出問題—— 怎麼解決呢?加 同步操作符:synchronize
 *                       a:是否多線程環境    是
 *                       b:是否有共享數據    是
 *                       c:是否有多條語句操作共享數據  是

 

==============================================================

==============================================================

1、單例模式的概述:

Java中單例模式(Singleton)是一種廣泛使用的設計模式。單例模式的主要作用是保證在Java程序中,某個類只有一個實例存在。一些管理器和控制器常被設計成單例模式。

2、單例模式的好處:

  1. 它能夠避免實例對象的重複創建,不僅可以減少每次創建對象的時間開銷,還可以節約內存空間;
  2. 它能夠避免由於操作多個實例導致的邏輯錯誤;
  3. 如果一個對象有可能貫穿整個應用程序,而且起到了全局統一管理控制的作用,那麼單例模式值得考慮。

3、單例1——餓漢模式(推薦)

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

(1)從代碼中我們看到,類的構造函數定義爲private的,保證其他類不能實例化此類;然後提供了一個靜態實例,並返回給調用者。餓漢模式是最簡單的一種實現方式,餓漢模式在類加載的時候就對創建實例,實例在整個程序週期都存在。

(2)優點:只在類加載的時候創建一次實例,不會存在多個線程創建多個實例的情況,避免了多線程同步的問題。

(3)缺點:即使這個類的實例沒有用到也會被創建,而且在類加載之後就被創建,內存被浪費了。

(4)使用場景:這種實現方式適合類的實例佔用內存比較小、在初始化時就會被用到的情況。但是,如果單例佔用的內存比較大、或者單例只是在某個特定場景下才會用到,使用餓漢模式就不合適了,這時候就需要用到懶漢模式進行延遲加載。

4、單例2——懶漢模式(未加鎖)

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

(1)從代碼中我們看到,類的構造函數定義爲private的,保證其他類不能實例化此類;然後提供了一個靜態實例,並返回給調用者。懶漢模式也是比較簡單的一種實現方式。

(2)優點:懶漢模式中,單例是在需要的時候纔去創建,如果單例已經創建,再次調用這個獲取接口getInstance()將不會重新創建新的對象,而是直接返回之前已經創建好的對象。

(3)適用場景:如果某個單例使用的次數較少,並且創建單例消耗的資源較多,那麼就需要實現單例的按需創建,這個時候使用懶漢模式就是一個不錯的選擇。

(4)缺點:這裏的懶漢模式並沒有考慮線程安全問題,在多線程環境中,多個線程可能會併發調用它的getInstance()方法,從而導致同時創建多個實例,因此需要加鎖來解決線程同步問題,見5。

5、單例3——懶漢模式(加鎖、雙重校驗鎖、volatile)(推薦)

未加鎖的懶漢模式單例的缺點前面已經敘述了,即存在線程安全問題。爲了避免在多線程環境中多個線程同時創建多個實例,可以使用加鎖來解決,代碼如下所示:

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){
    }
    public static synchronized Singleton getInstance(){
        //Single Checked
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

(1)加鎖的懶漢模式,看起來既解決了線程安全問題,又實現了延遲加載,然而,它存在着性能問題,依然不夠完美。

(2)synchronized關鍵字是一個重量級鎖,synchronized修飾的同步方法比一般方法要慢很多。如果多次調用該類的getInstance()方法,累積的性能損耗就比較大了。因此,就有了雙重校驗鎖,先看下它的代碼實現:

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){
    }
    public static Singleton getInstance() {
        //Single Checked
        if (instance == null) {
            synchronized (Singleton.class) {
                //Double checked
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

(1)從雙重校驗鎖代碼可以看到,在同步代碼塊的外面多了一層單例對象是否爲空的判斷if (instance == null)。由於單例對象只需要創建一次,如果後面再次調用getInstance()方法只需要直接返回單例對象即可。因此,在大部分情況下,調用getInstance()都不會執行到同步代碼塊,從而提高了程序性能。

(2)不過還需要考慮一種情況:假如兩個線程A和B,線程A執行了if (instance == null)語句,它會認爲單例對象沒有創建;此時,調度器切到了線程B,線程B也執行到了同樣的語句if (instance == null),線程B也認爲單例對象沒有創建;然後,兩個線程依次執行同步代碼塊,並且分別創建了一個單例對象,此時會出現問題。爲了解決這個問題,還需要在同步代碼塊中再增加一個單例對象是否爲空的判斷if (instance == null),也就是上面看到的代碼中的double checked。

(3)可以看到,雙重校驗鎖既實現了延遲加載,又解決了線程併發問題,同時還解決了執行效率問題,是否真的就萬無一失了呢?雙重校驗鎖的隱患:指令重排優化導致的雙重校驗鎖失效。

  • 這裏要提到Java中的指令重排優化。所謂的指令重排優化,就是指在不改變原語義的情況下,通過調整指令的執行順序讓java程序運行的更快。但是,JVM中並沒有規定編譯器優化相關的具體內容,也就是說,JVM可以根據實際情況,自由的進行指令重排序優化。
  • 雙重校驗鎖的隱患,其關鍵就在於:由於指令重排優化的存在,導致初始化Singleton對象和將對象地址賦給instance字段的順序是不確定的。比如:某個線程創建單例對象時,在構造方法被調用之前,就爲該單例對象分配了內存空間並將該單例對象的屬性設置爲默認值,此時,就可以將分配的內存地址賦值給instance字段了,然而,該對象可能還沒有被初始化。若緊接着另外一個線程來調用getInstance(),取到的就是狀態不正確的單例對象,程序就會出錯。

(4)以上就是雙重校驗鎖會失效的原因,不過還好,在JDK1.5及之後的版本增加了volatile關鍵字。volatile的一個語義就是:禁止指令重排序優化,也就保證了instance變量被賦值的時候類的單例對象已經初始化好了,從而避免了上面說到的問題。

PS:Java中的volatile關鍵字是什麼?

  1. 理解volatile關鍵字作用的前提,是要理解Java內存模型,volatile關鍵字的作用主要有兩個:(1)多線程主要圍繞可見性和原子性兩個特性而展開,使用volatile關鍵字修飾的變量,保證了其在多線程之間的可見性,即每次讀取到volatile變量,一定是最新的數據。(2)代碼底層執行不像我們看到的高級語言—-Java程序這麼簡單,它的執行是Java代碼–>字節碼–>根據字節碼執行對應的C/C++代碼–>C/C++代碼被編譯成彙編語言–>和硬件電路交互,現實中,爲了獲取更好的性能,JVM可能會對指令進行重排序,多線程下可能會出現一些意想不到的問題。使用volatile則會對禁止語義重排序,當然這也一定程度上降低了代碼執行效率。
  2. 從實踐角度而言,volatile的一個重要作用就是和CAS(compare and swap)結合,保證了變量的原子性。詳細的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger。(1)CAS,語義是比較和替換,它是設計併發算法時用到的一種技術。(2)簡單來說,比較和替換是使用一個期望值和一個變量的當前值進行比較,如果當前變量的值與我們期望的值相等,就使用一個新值替換當前變量的值。

(1)volatile是一個特殊的修飾符,只有成員變量才能使用它。在Java併發程序缺少同步類的情況下,多線程環境中,對volatile成員變量的操作對其它線程是透明的。(2)volatile變量可以保證下一個讀取操作會在上一個寫操作之後發生(參考: http://blog.csdn.net/fly910905/article/details/79283557)。

基於“雙重校驗鎖實現的懶漢式單例”有可能會出現指令重排導致鎖失效的背景,下面給出瞭解決方案:類的靜態屬性“單例對象”加上volatile關鍵字(禁止指令重排優化)。

public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton(){
    }
    public static Singleton getInstance() {
        //Single Checked
        if (instance == null) {
            synchronized (Singleton.class) {
                //Double checked
                if (instance == null) {
                    instance = new Singleton();
                }
 
            }
        }
        return instance;
    }
}

6、單例4——靜態內部類模式(推薦)

除了上面三種方式,還有另外一種實現單例的方式,通過靜態內部類來實現。首先看一下它的實現代碼:

public class Singleton{
    private static class SingletonHolder{
        public static Singleton instance = new Singleton();
    }
    private Singleton(){
    }
    public static Singleton newInstance(){
        return SingletonHolder.instance;
    }
}

這種方式同樣利用了類加載機制來保證只創建一個instance實例。它與餓漢模式一樣,也是利用了類加載機制,因此不存在多線程併發的問題。不一樣的是,它是在內部類裏面去創建單例對象。這樣的話,只要應用程序中不使用這個單例對象,JVM就不會去加載這個單例對象,也就不會創建單例對象,從而實現懶漢式的延遲加載,也就是說這種方式可以同時保證延遲加載和線程安全。

7、單例5——枚舉模式(推薦)


public class EnumSingleton{
    private EnumSingleton(){
    }
    public static EnumSingleton getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    private static enum Singleton{
        INSTANCE;
        private EnumSingleton singleton;
        //JVM會保證此方法絕對只調用一次
        private Singleton(){
            singleton = new EnumSingleton();
        }
        public EnumSingleton getInstance(){
            return singleton;
        }
    }
    public static void main(String[] args) {
        EnumSingleton obj1 = EnumSingleton.getInstance();
        EnumSingleton obj2 = EnumSingleton.getInstance();
        //輸出結果:obj1==obj2?true
        System.out.println("obj1==obj2?" + (obj1==obj2));
    }
}
  • 上面的類EnumSingleton是我們要應用單例模式的資源,具體可以表現爲網絡連接,數據庫連接,線程池等等。 
  • 獲取資源的方式很簡單,只要 Singleton.INSTANCE.getInstance() 即可獲得所要實例。
  • 下面我們來看看單例是如何被保證的: 
    • 首先,在枚舉中,我們明確了構造方法限制爲私有,在我們訪問枚舉實例的時候會執行這個構造方法。
    • 同時,每個枚舉實例都是static final類型的,也就表明只能被實例化一次。在調用構造方法時,我們的單例被實例化。 也就是說,因爲JVM會保證Enum中的實例只會被實例化一次,所以我們的INSTANCE也被保證實例化一次。 

最開始提到的四種實現單例的方式都有共同的缺點:

(1)需要額外的工作來實現序列化,否則,每次反序列化一個序列化的對象時都會創建一個新的實例。

(2)可以使用反射強行調用私有構造器(如果要避免這種情況,可以修改構造器,讓它在創建第二個實例的時候拋異常)。

而枚舉類很好的解決了這兩個問題,使用枚舉模式的單例模式,除了線程安全和防止反射調用私有構造器之外,還提供了自動序列化機制,防止反序列化的時候創建新的對象,強烈推薦!最後借用 《Effective Java》一書中的話:單元素的枚舉類型已經成爲實現Singleton的最佳方法!

 

8、單例模式的線程安全性

首先要說的是,單例模式的線程安全意味着:某個類的實例在多線程環境下只會被創建出來一次。

單例模式有很多種的寫法,我總結一下:

(1)餓漢式:線程安全

(2)懶漢式:非線程安全

(3)雙檢鎖:線程安全

(4)靜態內部類:線程安全

(5)枚舉:線程安全

 

參考來源:

(1)http://www.importnew.com/12773.html

(2)http://blog.csdn.net/goodlixueyong/article/details/51935526

(3)https://blog.csdn.net/fly910905/article/details/79286680

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章