設計模式第一章——單例模式

首先,在談設計模式之前,要知道一下設計模式的七大原則。
七大設計原則

  • 【開閉原則】是總綱,它告訴我們要【對擴展開放,對修改關閉】;
  • 【里氏替換原則】告訴我們【不要破壞繼承體系】;
  • 【依賴倒置原則】告訴我們要【面向接口編程】;
  • 【單一職責原則】告訴我們實現【類】要【職責單一】;
  • 【接口隔離原則】告訴我們在設計【接口】的時候要【精簡單一】;
  • 【迪米特法則】告訴我們要【降低耦合度】;
  • 【合成複用原則】告訴我們要【優先使用組合或者聚合關係複用,少用繼承關係複用】。

單例模式

首先問一下自己,單例模式是什麼?**
簡單點的回答就是在整個應用範圍內,同一個類只能有一個實例對象存在。

接下來我們就通過代碼來逐個講解單例的實現。

單例模式的實現有兩種方式: 餓漢式 和 懶漢式。

- 餓漢式單例:

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 餓漢式單例模式
 * 特點:類初始化的時候就創建好。但是浪費內存空間,
 * 沒有線程安全問題,因爲JVM在類初始化的時候是互斥的。
 */
public class HPerson {

    //1. 私有化構造方法
    private HPerson(){}


    //2. 靜態成員變量
    private static  HPerson hPerson = new HPerson();

    //3. 對外提供方法,獲取類對象。(調用私有構造方法,獲取類對象)
    public  HPerson gethPerson(){
        return hPerson;
    }

    //4. 普通類方法
    public void sayHelle(){
        System.out.println("我是餓漢式單例");
    }

}

餓漢式單例在性能和空間上有一些缺點,這是需要注意的。一般我們推薦使用懶漢式單例。

懶漢式單例:

懶漢式單例有許多實現方式,接下來我會寫6個實現方式,逐步遞進,從線程安全的問題開始切入,最後再從反射攻擊和序列化攻擊進行完善。

01

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懶漢式單例01 (非線程安全的)
 *
 * 餓漢式單例精髓就是延遲加載,當你需要的時候再創建這個類對象。
 *
 * 懶漢式設計模式,有一種叫法:延遲加載
 *
 *
 * 懶漢式單例模式步驟:
 * 		1:構造私有
 * 		2:定義私有靜態成員變量,先不初始化
 * 		3:定義公開靜態方法,獲取本身對象
 * 			有對象就返回已有對象
 * 			沒有對象,再去創建
 *
 * 線程安全問題,判斷依據:
 * 		1:是否存在多線程	是
 * 		2:是否有共享數據	是
 * 		3:是否存在非原子性操作
 *
 *
 */
public class LPerson01 {


    //  1.私有化構造函數
    private LPerson01(){}

    //  2. 定義私有靜態成員變量 ,不初始化
    private static LPerson01 lPerson01;

    //  3.對外獲取對象的接口,內部判斷是否爲空
    public LPerson01 lPerson01Factory(){
        if(lPerson01 == null){
            lPerson01 = new LPerson01();
        }
        return lPerson01;
    }

}

02


/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懶漢式單例02
 * 在01的基礎上在方法上進行了加鎖
 * 特點:線程安全,但是除首次創建以外,都進行了多餘的加鎖,性能不好
 *
 */
public class LPerson02 {


    //  1.私有化構造函數
    private LPerson02(){}

    //  2. 定義私有靜態成員變量 ,不初始化
    private static LPerson02 lPerson02;

    //  3.對外獲取對象的接口,內部判斷是否爲空,加鎖
    public synchronized  LPerson02 lPerson01Factory(){
        if(lPerson02 == null){
            lPerson02 = new LPerson02();
        }
        return lPerson02;
    }

}

03 雙重檢查鎖(雙重校驗鎖)方式實現


/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懶漢式單例03
 * 在02的基礎上進行了雙重檢查
 * 特點:似乎解決了之前提到的問題,將synchronized關鍵字加在了內部,也就是說當調用的時候是不需要加鎖的,
 * 只有在instance爲null,並創建對象的時候才需要加鎖,性能有一定的提升。
 *
 * 但是,由於指令重排序的存在,這樣的情況,還是有可能有問題的。看下面的情況:
 * 	在Java指令中創建對象和賦值操作是分開進行的,也就是說instance = new Singleton();語句是分兩步執行的。
 *  但是JVM並不保證這兩個操作的先後順序,也就是說有可能JVM會爲新的Singleton實例分配空間,然後直接賦值給instance成員,
 *  然後再去初始化這個Singleton實例。
 *
 *  這樣就可能出錯了,我們以A、B兩個線程爲例:
 *
 * 		a> A、B線程同時進入了第一個if判斷
 *
 * 		b> A首先進入synchronized塊,由於instance爲null,所以它執行instance = new Singleton();
 *
 * 		c> 由於JVM內部的優化機制(指令重排序),JVM先畫出了一些分配給Singleton實例的空白內存,
 * 	     	並賦值給instance成員(注意此時JVM沒有開始初始化這個實例),然後A離開了synchronized塊。
 *
 * 		d> B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給調用該方法的程序。
 *
 * 		e> 此時B線程打算使用Singleton實例,卻發現它沒有被初始化,於是錯誤發生了。
 *
 *
 */
public class LPerson03 {


    //  1.私有化構造函數
    private LPerson03(){}

    //  2. 定義私有靜態成員變量 ,不初始化
    private static LPerson03 lPerson03;

    //  3.對外獲取對象的接口,內部判斷是否爲空,雙重檢查鎖,內部加鎖
    public LPerson03 lPerson01Factory(){
        if(lPerson03 == null){
            synchronized(LPerson03.class){
                if(lPerson03 == null)
                    lPerson03 = new LPerson03();
            }
        }
        return lPerson03;
    }

}

04

package singleton;

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懶漢式單例04
 * 在03的基礎上運用了 volatile 修飾靜態成員變量,阻止指令重排序。
 * 特點:volatile關鍵字 基本滿足所有要求了。但是還是可以通過反射或者序列化的方式攻擊
 *
 */
public class LPerson04 {


    //  1.私有化構造函數
    private LPerson04(){}

    //  2. 定義私有靜態成員變量 ,不初始化
    private static  volatile  LPerson04 lPerson04;

    //  3.對外獲取對象的接口,內部判斷是否爲空,雙重檢查鎖,內部加鎖
    public synchronized LPerson04 lPerson01Factory(){
        if(lPerson04 == null){
            synchronized(LPerson04.class){
                if(lPerson04 == null)
                    lPerson04 = new LPerson04();
            }
        }
        return lPerson04;
    }

}

05 靜態內部類方式

package singleton;

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懶漢式單例05
 * 通過內部類來實現
 * 特點:線程安全,效率也高,但是可以通過序列化攻擊
 * JVM在類加載的時候,是互斥的,所以可以由此保證線程安全問題
 */
public class LPerson05 {


    //  1.私有化構造函數
    private LPerson05(){}

    //  2. 定義靜態內部類
    private static class LPerson05Factory{
        private static LPerson05 lPerson05 = new LPerson05();
    }

   //   3. 對外接口提供獲取實例的入口。
    public  static LPerson05 getInstance(){
        return LPerson05Factory.lPerson05;
    }

}

06 枚舉方式實現

package singleton;

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懶漢式單例06 (最簡單最安全)
 * 枚舉方法實現
 * 特點:基本滿足所有要求,而且還不容易被攻擊。
 *
 */
public enum LPerson06 {
    //    1. 類聲明
    LPerson06("枚舉單例");

    private String name;

    private LPerson06(String name){
       this.name = name;
    }

    //    2. 類的內部方法
    public void sayHello() {
        System.out.println("這是通過枚舉實現的單例");
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

至於反射和序列化是如何攻擊或者說獲取實例的,我們簡單寫一下,可粗略看一眼。

//** 反射攻擊
			public class SingletonAttack {
				public static void main(String[] args) throws Exception {
					reflectionAttack();
				}

				public static void reflectionAttack() throws Exception {
					//1,通過反射,獲取單例類的私有構造器
					Constructor constructor = DoubleCheckLockSingleton.class.getDeclaredConstructor();
					//2,設置私有成員的暴力破解
					constructor.setAccessible(true);
					//3,通過反射去創建單例類的多個不同的實例
					DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton)constructor.newInstance();
					DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)constructor.newInstance();
					
					s1.tellEveryone();
					s2.tellEveryone();
					System.out.println(s1 == s2);
				}
			}
		
//** 序列化攻擊
			public class SingletonAttack {
				public static void main(String[] args) throws Exception {
					serializationAttack();
				}
				  /**
					* 通過將單例實例寫入文件,然後再通過文件讀取創建
					*/
				public static void serializationAttack() throws Exception {
					//01 讀取文件,用對象序列化流去對對象進行操作
					ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serFile"));
					//02 通過單例代碼獲取一個對象
					DoubleCheckLockSingleton s1 = DoubleCheckLockSingleton.getInstance();
					//03 將單例對象,通過序列化流,序列化到文件中
					outputStream.writeObject(s1);
					
					//04 通過序列化流,將文件中序列化的對象信息讀取到內存中
					ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("serFile")));
					//05 通過序列化流,去創建對象
					DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)inputStream.readObject();
					
					s1.tellEveryone();
					s2.tellEveryone();
					
					System.out.println(s1 == s2);
				}
			}

防止序列化攻擊也可以通過在單例類中重寫readResolve方法實現:

private Object readResolve() {
  return instance;
}

最後再補充一些關於線程安全的內容:

  • 併發編程的三大特性

  • 原子性
    *** 狹義上指的是CPU操作指令必須是原子操作
    *** 廣義上指的是字節碼指令是原子操作
    *** 如何保證原子性呢?加鎖(synchronize、Lock)

  • 有序性
    *** 狹義上指的是CPU操作指令是有序執行的
    *** 廣義上指的是字節碼指令是有序執行的
    *** 指令重排序(JIT即時編譯器的優化策略)
    ** happend-before六大原則
    ** 兩行代碼之後的操作,執行結果不存在影響,就可以發生指令重排序(JMM課程)

     				int i =10;
     				boolean a = false;
     				
     				i ++ ; // 操作1
     				a = true; // 操作2
    

3.可見性
*** 在多核(CPU)時代,內存的可見性是一個很常見的併發問題。
*** 可見性的解決需要使用到volatile關鍵字

其他信息:

  • 對象在JVM中的創建步驟 Student student = new Student();

    • 1.new:開闢JVM堆中的內存空間
    • 2.將內存空間初始化(指的就是對象的成員變量初始化爲0值)
    • 3.將內存空間地址(引用地址)賦值給引用類型的變量
    • 結論:在new對象的時候,JIT即時編譯器會根據運行情況,對對象創建的過程進行指令重排序(132)
  • 線程執行代碼的時候需要通過競爭CPU時間片去執行

  • volatile關鍵字

    • 作用一:禁止被它修飾的變量發生指令重排操作。是通過內存屏障去完成的禁止指令重排序。
    • 作用二:簡單理解是禁止CPU緩存使用,其實是被volatile關鍵字修飾的變量,在修改之前,都需要將最新CPU緩存中的數據刷新到主內存中。

以上內容爲開課吧學習總結。
附上自己代碼的git地址:https://github.com/ying105525/ysl-design-pattern.git

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