一、單例模式的應用
單例對象(Singleton)是一種常用的設計模式。在 Java 應用中,單例對象能保證在一個 JVM中,該對象隻有一個執行個體存在。這樣的模式有幾個好處:
- 1、某些類建立比較頻繁,對于一些大型的對象,這是一筆很大的系統開銷。
- 2、省去了 new 操作符,降低了系統記憶體的使用頻率,減輕 GC 壓力。
- 3、有些類如交易所的核心交易引擎,控制着交易流程,如果該類可以建立多個的話,系統完全亂了。(比如一個軍隊出現了多個司令員同時指揮,肯定會亂成一團),是以隻有使用單例模式,才能保證核心交易伺服器獨立控制整個流程。
單例模式的應用有
- 網站的計數器,一般也是單例模式實作,否則難以同步。
- 應用程式的日志應用,一般都使用單例模式實作,這一般是由于共享的日志。檔案一直處于打開狀态,因為隻能有一個執行個體去操作,否則内容不好追加。
- 資料庫連接配接池的設計一般也是采用單例模式,因為資料庫連接配接是一種資料庫資源。
- 項目中,讀取配置檔案的類,一般也隻有一個對象。沒有必要每次使用配置檔案資料,都生成一個對象去讀取。
- Application 也是單例的典型應用
- Windows的Task Manager (任務管理器)就是很典型的單例模式
- Windows的Recycle Bin (資源回收筒)也是典型的單例應用。在整個系統運作過程中,資源回收筒一直維護着僅有的一個執行個體。
二、單例模式的實作
單例的實作主要是通過以下兩個步驟:
- 将該類的構造方法定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造方法來執行個體化該類的對象,隻有通過該類提供的靜态方法來得到該類的唯一執行個體;
- 在該類内提供一個靜态方法,當我們調用這個方法時,如果類持有的引用不為空就傳回這個引用,如果類保持的引用為空就建立該類的執行個體并将執行個體的引用賦予該類保持的引用。
1、饑漢式
// 餓漢式單例
public final class Singleton implements Serializable {
// 指向自己執行個體的私有靜态引用,主動建立
private static Singleton INSTANCE = new Singleton();
// 私有的構造方法
private Singleton(){}
// 以自己執行個體為傳回值的靜态的公有方法,靜态工廠方法
public static Singleton1 getSingleton1(){
return INSTANCE ;
}
public Object readResolve() {
return INSTANCE;
}
}
問題1 : 為什麼加 final?
加final為了防止有子類, 因為子類可以重寫父類的方法
問題2 : 如果實作了序列化接口, 還要做什麼來防止反序列化破壞單例?
首先通過反序列化操作, 也是可以建立一個對象的, 破壞了單例, 可以使用readResolve方法并傳回instance對象, 當反序列化的時候就會調用自己寫的readResolve方法
問題3 : 為什麼設定為私有?
私有化構造器, 防止外部通過構造器來建立對象; 但不能防止反射來建立對象
設定為私有是否能防止反射建立新的執行個體?
不能。暴力反射
問題4 : 這樣初始化是否能保證單例對象建立時的線程安全?
因為單例對象是static的, 靜态成員變量的初始化操作是在類加載階段完成, 由JVM保證其線程安全 (這其實是利用了ClassLoader的線程安全機制。ClassLoader的loadClass方法在加載類的時候使用了synchronized關鍵字。)
問題5 : 為什麼提供靜态方法而不是直接将 INSTANCE 設定為 public?
通過向外提供公共方法, 展現了更好的封裝性, 可以在方法内實作懶加載的單例; 可以提供泛型等補充 : 任何一個readObject方法,不管是顯式的還是預設的,它都會傳回一個建立的執行個體,這個建立的執行個體不同于該類初始化時建立的執行個體。
枚舉類實作
枚舉的變量, 底層是通過public static final來修飾的, 類加載就建立了,是以是餓漢式
// 問題1:枚舉單例是如何限制執行個體個數的:建立枚舉類的時候就已經定義好了,每個枚舉常量其實就是枚舉類的一個靜态成員變量
// 問題2:枚舉單例在建立時是否有并發問題:沒有,枚舉單例底層是靜态成員變量,它是通過類加載器的加載而建立的, 確定了 線程安全
// 問題3:枚舉單例能否被反射破壞單例:不能
// 問題4:枚舉單例能否被反序列化破壞單例:枚舉類預設實作了序列化接口,枚舉類已經考慮到此問題,無需擔心破壞單例
// 問題5:枚舉單例屬于懶漢式還是餓漢式:餓漢式
// 問題6:枚舉單例如果希望加入一些單例建立時的初始化邏輯該如何做:加構造方法就行了
enum Singleton {
INSTANCE;
}
小結
我們知道,類加載的方式是按需加載,且加載一次。是以,在上述單例類被加載時,就會執行個體化一個對象并交給自己的引用,供系統使用;而且,由于這個類在整個生命周期中隻會被加載一次,是以隻會建立一個執行個體,即能夠充分保證單例。
- 優點:這種寫法比較簡單,就是在類裝載的時候就完成執行個體化。避免了線程同步問題。
- 缺點:在類裝載的時候就完成執行個體化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個執行個體,則會造成記憶體的浪費。
2、懶漢式
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
我們從懶漢式單例可以看到,單例執行個體被延遲加載,即隻有在真正使用的時候才會執行個體化一個對象并交給自己的引用。
這種寫法起到了Lazy Loading的效果,但是隻能在單線程下使用。如果在多線程下,一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會産生多個執行個體。是以在多線程環境下不可使用這種方式。
餓漢式和懶漢式的局别也顯而易見
餓漢式:
- 壞處:對象加載時間過長。
- 好處:餓漢式是線程安全的
懶漢式:
- 好處:延遲對象的建立。
- 目前的寫法壞處:線程不安全。--->到多線程内容時,再修改
像懶漢式這樣毫無線程安全保護的類,如果我們把它放入多線程的環境下,肯定就會出現問題了,如何解決?我們首先會想到對 getSingleton方法加synchronized 關鍵字,如下
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
但是,synchronized關鍵字鎖住的是這個對象,這樣的用法,在性能上會有所下降,因為每次調用getInstance(),都要對對象上鎖,事實上,隻有在第一次建立對象的時候需要加鎖,之後就不需要了,是以,這個地方需要改進。我們改成下面這個:
3、雙重鎖機制實作單例
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
似乎解決了之前提到的問題,将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執行個體,卻發現它沒有被初始化,于是錯誤發生了。
為了解決上述構造方法指令出現重排序的問題,對 INSTANCE 使用 volatile 修飾,可以禁用指令重排。
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 執行個體沒建立,才會進入内部的 synchronized代碼塊
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也許有其它線程已經建立執行個體,是以再判斷一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
單例模式使用内部類來維護單例的實作,JVM内部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance隻被建立一次,并且會保證把指派給instance的記憶體初始化完畢,這樣我們就不用擔心上面的問題。同時該方法也隻會在第一次調用的時候使用互斥機制,這樣就解決了低性能問題。這樣我們暫時總結一個完美的單例模式:
4、靜态内部類
public class Singleton {
/* 私有構造方法,防止被執行個體化 */
private Singleton() {
}
/* 此處使用一個内部類來維護單例*/
// 問題1:屬于懶漢式還是餓漢式:懶漢式,這是一個靜态内部類。類加載本身就是懶惰的,在沒有調用getInstance方法時是沒有執行LazyHolder内部類的類加載操作的。
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}
/* 擷取執行個體 */
// 問題2:在建立時是否有并發問題,這是線程安全的,類加載時,jvm保證類加載操作的線程安全
public static Singleton getInstance() {
return SingletonFactory.instance;
}
/* 如果該對象被用于序列化,可以保證對象在序列化前後保持一緻 */
public Object readResolve() {
return getInstance();
}
}
其實說它完美,也不一定,如果在構造函數中抛出異常,執行個體将永遠得不到建立,也會出錯。是以說,十分完美的東西是沒有的,我們隻能根據實際情況,選擇最适合自己應用場景的實作方法。也有人這樣實作:因為我們隻需要在建立類的時候進行同步,是以隻要将建立和getInstance()分開,單獨為建立加synchronized關鍵字,也是可以的:
public class SingletonTest {
private static SingletonTest instance = null;
private SingletonTest() {
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new SingletonTest();
}
}
public static SingletonTest getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
}
考慮性能的話,整個程式隻需建立一次執行個體,是以性能也不會有什麼影響。
三、總結
從這四種實作中,我們可以總結出,要想實作效率高的線程安全的單例,我們必須注意以下兩點:
- 盡量減少同步塊的作用域;
- 盡量使用細粒度的鎖。
作者:王陸
出處:https://www.cnblogs.com/wkfvawl/
-------------------------------------------
個性簽名:罔談彼短,靡持己長。做一個謙遜愛學的人!
本站使用「署名 4.0 國際」創作共享協定,轉載請在文章明顯位置注明作者及出處。鑒于部落客處于考研複習期間,有什麼問題請在評論區中提出,部落客盡可能當天回複,加微信好友請注明原因