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

一、嘻哈說

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

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

某個類只有一個實例
並自行實例化向整個系統提供這個實例
須要私有構造方法毋庸置疑
自行實例化各有各的依據
提供單一實例則大致一致
餓漢靜態變量初始化實例
懶漢初始爲空
獲取實例爲空才建立一次
方法加上鎖弄成線程安全的例子
DCL雙重檢查鎖兩次判空加鎖讓併發不是難事
建立對象並非原子操做由於處理器亂序
volatile的關鍵字開始用武之地
靜態內部類中有一個單例對象的靜態的實例
枚舉天生單例
容器管理多個單例
複製代碼

試聽請點擊這裏設計模式

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

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

番茄課堂,學習也要酷。併發

二、定義

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

什麼是建立型模式?學習

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

設計模式能夠分爲三類:建立型模式、結構型模式、行爲型模式。ui

建立型模式:提供了一種在建立對象的同時隱藏建立邏輯的方式,而不是使用 new 運算符直接實例化對象。spa

結構型模式:關注類和對象的組合,用繼承的概念來組合接口和定義組合對象得到新功能的方式。

行爲型模式:關注對象之間的通訊。

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

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

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

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

三、特性

從定義中,咱們能夠分析出一些特性來:

單例類只能有一個實例

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

單例類必須自行建立本身的惟一的實例

自行實例化。

單例類必須給全部其餘對象提供這一實例 。 向整個系統提供這個實例。

內存中會長期持有單例實例,若是不是對全部對象提供訪問,例如只對包內類提供訪問權限,存在的意義就不大了。

四、套路

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

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

套路2:自行實例化,保證明例化在內存中只存在一份。

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

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

套路2則有不少靈活的實現方式,只要保證只實例化一次就是能夠的。

OK,那我開始擼代碼。

五、代碼

一、餓漢模式

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關鍵字。

優勢:類加載時會初始化單例對象,首次調用速度變快。

缺點:類加載時會初始化單例對象,容易產生垃圾。

二、懶漢模式

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()鎖後還線程安全呢?

三、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,雙重檢查鎖。所謂的雙重檢查就是兩次判空。

爲何要進行第二次判空,這不是脫褲子放屁,畫蛇添足嘛。

可能以爲它只是個屁,但實際上是竄稀,因此,脫褲子也是有必要的。

有這樣一種狀況,線程一、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中必要的代碼優化,因此在效率上比較低,所以必定在必要時才使用此關鍵字。

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

四、靜態內部類

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()返回靜態內部類的靜態單例對象

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

靜態內部類中的靜態變量是經過類加載器初始化的,也就是在內存中是惟一的,保證了單例。

線程安全:利用了類加載器的機制,肯線程安全

靜態內部類簡單,線程安全,懶加載,因此,強烈推薦

還有一個你們可能想象不到的實現方式,那就是枚舉。

五、枚舉

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方法並將單一實例返回,所以反序列化也會返回同一個實例。

六、容器

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))。

六、END

單例模式實現的方式雖然有不少,但都是爲了讓某一個類只有一個實例

今天就先說到這裏,下次是建造者模式,感謝你們。

相關文章
相關標籤/搜索