嘻哈說:設計模式之單例模式

番茄課堂_懶人發表於2018-09-20

1、嘻哈說

首先,請您欣賞單例模式的原創歌曲

嘻哈說:單例模式
作曲:懶人
作詞:懶人
Rapper:懶人

某個類只有一個例項
並自行例項化向整個系統提供這個例項
需要私有構造方法毋庸置疑
自行例項化各有各的依據
提供單一例項則大體一致
餓漢靜態變數初始化例項
懶漢初始為空
獲取例項為空才建立一次
方法加上鎖弄成執行緒安全的例子
DCL雙重檢查鎖兩次判空加鎖讓併發不是難事
建立物件並不是原子操作因為處理器亂序
volatile的關鍵字開始用武之地
靜態內部類中有一個單例物件的靜態的例項
列舉天生單例
容器管理多個單例
複製程式碼

試聽請點選這裡

閒來無事聽聽曲,知識已填腦中去;

學習複習新方式,頭戴耳機不小覷。

番茄課堂,學習也要酷。

2、定義

在Java設計模式中,單例模式相對來說算是比較簡單的一種建立型模式。

什麼是建立型模式?

建立型模式是設計模式的一種分類。

設計模式可以分為三類:建立型模式、結構型模式、行為型模式。

建立型模式:提供了一種在建立物件的同時隱藏建立邏輯的方式,而不是使用 new 運算子直接例項化物件。

結構型模式:關注類和物件的組合,用繼承的概念來組合介面和定義組合物件獲得新功能的方式。

行為型模式:關注物件之間的通訊。

我們來看一下單例模式的定義。

確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項

也就是,保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點

單例模式在懶人眼中就是,注孤生,悲慘世界

3、特性

從定義中,我們可以分析出一些特性來:

單例類只能有一個例項

確保某一個類只有一個例項,must be 呀。

單例類必須自行建立自己的唯一的例項

自行例項化。

單例類必須給所有其他物件提供這一例項 。 向整個系統提供這個例項。

記憶體中會長期持有單例例項,如果不是對所有物件提供訪問,例如只對包內類提供訪問許可權,存在的意義就不大了。

4、套路

怎樣確保某一個類只有一個例項?

套路1:私有化空構造方法,避免多處例項化。

套路2:自行例項化,保證例項化在記憶體中只存在一份。

套路3:提供公有靜態getInstance()方法,並將單一的例項返回。

套路1與套路3是固定的套路,基本不會有變。

套路2則有很多靈活的實現方式,只要保證只例項化一次就是可以的。

OK,那我開始擼程式碼。

5、程式碼

1、餓漢模式

package com.fanqiekt.singleton;

/**
 * 餓漢單例模式
 *
 * @author 番茄課堂-懶人
 */
public class EHanSingleton {

	private static EHanSingleton sInstance = new EHanSingleton();

	//私有化空構造方法
	private EHanSingleton() {}

	//靜態方法返回單例類物件
	public static EHanSingleton getInstance() {
		return sInstance;
	}

	//其他業務方法
	public void otherMethods(){
		System.out.println("餓漢模式的其他方法");
	}
}
複製程式碼

套路1:私有化空構造方法。

套路2:自行例項化,保證例項化在記憶體中只存在一份

實現方式:靜態例項變數的初始化

實現原理:類載入時就會初始化單例物件,並且只初始化一次。

套路3:提供公有靜態getInstance()方法,並將單一的例項返回。

為什麼叫餓漢?

因為餓漢很餓,需要儘早初始化來餵飽自己。

從執行緒安全,優缺點總結一下。

執行緒安全:利用類載入器的機制,肯定是執行緒安全的。

為什麼這麼說呢?

ClassLoader的loadClass方法在載入類的時候使用了synchronized關鍵字。

優點:類載入時會初始化單例物件,首次呼叫速度變快。

缺點:類載入時會初始化單例物件,容易產生垃圾。

2、懶漢模式

package com.fanqiekt.singleton;

/**
 * 懶漢模式
 *
 * @author 番茄課堂-懶人
 */
public class LazySingleton {

	private static LazySingleton sInstance;

	//私有化空構造方法
	private LazySingleton() {}

	//靜態方法返回單例類物件
	public static LazySingleton getInstance() {
		//懶載入
		if(sInstance == null) {
			sInstance = new LazySingleton();
		}
		return sInstance;
	}

	//其他業務方法
	public void otherMethods(){
		System.out.println("懶漢模式的其他方法");
	}
}
複製程式碼

套路1:私有化空構造方法。

套路2:自行例項化,保證例項化在記憶體中只存在一份

實現方式:getInstance()裡進行例項判空

實現原理:為空則建立例項;不為空,則直接返回例項。

套路3:提供公有靜態getInstance()方法,並將單一的例項返回。

為什麼叫懶漢?

因為懶漢懶惰,懶得初始化,用到了才開始初始化。

執行緒安全嗎?

很明顯,不是執行緒安全的,因為getInstance()方法沒有做任何的同步處理。

怎麼辦?

給getInstance()加鎖。

//靜態方法返回單例類物件,加鎖
	public static synchronized LazySingleton getInstance() {
		//懶載入
		if(sInstance == null) {
			sInstance = new LazySingleton();
		}
		return sInstance;
	}
複製程式碼

這樣就變成執行緒安全的懶漢模式了。

懶漢模式有什麼優缺點呢?

優點:第一次使用時才會初始化,節省資源

缺點:第一次使用時需要進行初始化,所以會變慢。給getInstance()加鎖後,getInstance()呼叫也會變慢。

那有沒有辦法可以去掉getInstance()鎖後還執行緒安全呢?

3、DCL

package com.fanqiekt.singleton;

/**
 * Double Check Lock 單例
 *
 * @author 番茄課堂-懶人
 */
public class DCLSingleton {

	private static DCLSingleton sInstance;

	//私有化空構造方法
	private DCLSingleton() {}

	//靜態方法返回單例類物件
	public static DCLSingleton getInstance() {
		//兩次判空
		if(sInstance == null) {
			synchronized(DCLSingleton.class) {
				if(sInstance == null) {
					sInstance = new DCLSingleton();
					return sInstance;
				}
			}
		}
		return sInstance;
	}

	//其他業務方法
	public void otherMethods(){
		System.out.println("DCL模式的其他方法");
	}
}
複製程式碼

與懶漢模式的區別在於:

去掉getInstance()方法上的鎖,在方法內部例項為空後再進行加鎖。

好處:只有當例項沒有初始化的情況下才會同步鎖,避免了給getInstance()整個方法加鎖的情況。

dcl的全稱是Double Check Lock,雙重檢查鎖。所謂的雙重檢查就是兩次判空。

為什麼要進行第二次判空,這不是脫褲子放屁,多此一舉嘛。

可能覺得它只是個屁,但其實是竄稀,所以,脫褲子也是有必要的。

有這樣一種情況,執行緒1、2同時判斷第一次為空,在加鎖的地方的阻塞了,如果沒有第二次判空,那麼執行緒1執行完畢後執行緒2就會再次執行,這樣就初始化了兩次,就存在問題了。

兩次判空後,DCL就安全多了,一般不會存在問題。但當併發量特別大的時候,還是會存在風險的。

在哪裡呢?

sInstance = new DCLSingleton()這裡。

是不是很奇怪,這句很普通的建立例項的語句怎麼會有風險。

情況是這樣的:

sInstance = new DCLSingleton()並不是一個原子操作,它轉換成了多條彙編指令,大致做了3件事情:

第一步:分配記憶體。

第二步:呼叫構造方法初始化。

第三步:將sInstanc物件指向分配空間。

由於Java編譯器允許處理器亂序執行,所以這三步順序不定,如果依次執行肯定沒問題,但如果執行完第一步和第三步後,其他的執行緒使用sInstanc就會報錯。

那如何解決呢?

這裡就需要用到關鍵字volatile了。

volatile有什麼用呢?

第一個:實現可見性。

什麼意思呢?

在當前的Java記憶體模型下,執行緒可以把變數儲存在本地記憶體(比如機器的暫存器)中,而不是直接在主存中進行讀寫。

這就可能造成一個執行緒在主存中修改了一個變數的值,而另外一個執行緒還繼續使用它在暫存器中的變數值的拷貝,造成資料的不一致。

volatile在這個時候就派上用場了。

讀volatile:每當子執行緒某一語句要用到volatile變數時,都會從主執行緒重新拷貝一份,這樣就保證子執行緒的會跟主執行緒的一致。

寫volatile: 每當子執行緒某一語句要寫volatile變數時,都會在讀完後同步到主執行緒去,這樣就保證主執行緒的變數及時更新。

第二個:防止處理器亂序執行。

volatile變數初始化的時候,就只能第一步、第二步、第三步這樣的順序執行了。

所以我們可以把sInstance的變數宣告的程式碼更改下。

private volatile static DCLSingleton sInstance;
複製程式碼

不過,由於使用volatile遮蔽掉了JVM中必要的程式碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。

感覺實現起來有點複雜,那有沒有一樣優秀還更簡單點的單例模式?

4、靜態內部類

package com.fanqiekt.singleton;

/**
 * 靜態內部類單例模式
 *
 * @author 番茄課堂-懶人
 */
public class StaticSingleton {

	//私有靜態單例物件
	private StaticSingleton() {}

	//靜態方法返回單例類物件
	public static StaticSingleton getInstance() {
		return SingleHolder.INSTANCE;
	}

	//單例類中存在一個靜態內部類
	private static class SingleHolder {
		//靜態類中存在靜態單例宣告與初始化
		private static final StaticSingleton INSTANCE = new StaticSingleton();
	}

	//其他業務方法
	public void otherMethods(){
		System.out.println("靜態內部類的其他方法");
	}
}
複製程式碼

套路1:私有化空構造方法。

套路2:自行例項化,保證例項化在記憶體中只存在一份

實現方式:宣告一個靜態內部類,靜態內部類中有個單例物件的靜態例項,getInstance()返回靜態內部類的靜態單例物件

實現原理:內部類不會在其外部類被載入的時候被載入,只有當內部類被使用的時候才會被使用。這樣就避免了類載入的時候就被初始化,屬於懶載入。

靜態內部類中的靜態變數是通過類載入器初始化的,也就是在記憶體中是唯一的,保證了單例。

執行緒安全:利用了類載入器的機制,肯執行緒安全

靜態內部類簡單,執行緒安全,懶載入,所以,強烈推薦

還有一個大家可能想象不到的實現方式,那就是列舉。

5、列舉

package com.fanqiekt.singleton;

/**
 * 列舉單例模式
 *
 * @Author: 番茄課堂-懶人
 */
public enum EnumSingleton {
    INSTANCE;

    //其他業務方法
    public void otherMethods(){
        System.out.println("列舉模式的其他方法");
    }
}
複製程式碼

列舉的特點:

保證只有一個例項。

執行緒安全。

自由序列化。

可以說列舉就是一個天生的單例,而且還可以自由序列化,反序列化後也是單例的。

而上邊幾種單例方式反序列化後是會重新再生成物件的,這就是列舉的強大之處。 那列舉的原理是什麼呢?

我們可以看一下生成的列舉反編譯一下,我在這裡只貼上下核心部分。

public final class EnumSingleton extends Enum{
    private EnumSingleton(){}

    static {
        INSTANCE = new EnumSingleton();
    }
}
複製程式碼

Enum就是一個普通的類,它繼承自java.lang.Enum類。所以,列舉具有類的所有功能。

他的實現方式優點類似於餓漢模式。

而且,程式碼還做了一些其他的事情,例如:重寫了readResolve方法並將單一例項返回,因此反序列化也會返回同一個例項。

6、容器

package com.fanqiekt.singleton;

import java.util.HashMap;
import java.util.Map;

/**
 * 容器單例模式
 *
 * @Author: 番茄課堂-懶人
 */
public class SingletonManager {

    private static Map<String, Object> objectMap = new HashMap<>();

    //私有化空構造方法
    private SingletonManager(){}

    //將單例的物件註冊到容器中
    public static void registerService(String key, Object instance){
        if(!objectMap.containsKey(key)){
            objectMap.put(key, instance);
        }
    }

    //從容器中獲得單例物件
    public static Object getService(String key){
        return objectMap.get(key);
    }
}

複製程式碼

實現方式:一個靜態的Map,一個將物件放到map的方法,一個獲取map中物件的方法

實現原理:根據key存物件,如果map中已經存在key,則不放入map;不存在key,則放入map,這樣可以保證每個key對應的物件為單一例項。

容器單例的最大好處是,可以管理多個單例。

Android原始碼中就用到了這種方式,通過Context獲取系統級別的服務(context.getSystemService(key))。

6、END

單例模式實現的方式雖然有很多,但都是為了讓某一個類只有一個例項

今天就先說到這裡,下次是建造者模式,感謝大家。

相關文章