首先,請您欣賞單例模式的原創歌曲。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))。
單例模式實現的方式雖然有不少,但都是爲了讓某一個類只有一個實例。
今天就先說到這裏,下次是建造者模式,感謝你們。