首先,在談設計模式之前,要知道一下設計模式的七大原則。
七大設計原則
- 【開閉原則】是總綱,它告訴我們要【對擴展開放,對修改關閉】;
- 【里氏替換原則】告訴我們【不要破壞繼承體系】;
- 【依賴倒置原則】告訴我們要【面向接口編程】;
- 【單一職責原則】告訴我們實現【類】要【職責單一】;
- 【接口隔離原則】告訴我們在設計【接口】的時候要【精簡單一】;
- 【迪米特法則】告訴我們要【降低耦合度】;
- 【合成複用原則】告訴我們要【優先使用組合或者聚合關係複用,少用繼承關係複用】。
單例模式
首先問一下自己,單例模式是什麼?**
簡單點的回答就是在整個應用範圍內,同一個類只能有一個實例對象存在。
接下來我們就通過代碼來逐個講解單例的實現。
單例模式的實現有兩種方式: 餓漢式 和 懶漢式。
- 餓漢式單例:
/**
* 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