一、單例模式的應用
單例對象(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 : 為什么設置為私有?
私有化構造器, 防止外部通過構造器來創建對象; 但不能防止反射來創建對象
設置為私有是否能防止反射創建新的實例?
不能。暴力反射,但可以在改造構造方法,就可以防止反射破壞單例
private Singleton() {
if (INSTANCE != null) {
//防止反射破壞單例
throw new RuntimeException("單例對象不能重復創建");
}
System.out.println("private Singleton1()");
}
問題4 : 這樣初始化是否能保證單例對象創建時的線程安全?
因為單例對象是static的, 靜態成員變量的初始化操作是在類加載階段完成, 由JVM保證其線程安全 (這其實是利用了ClassLoader的線程安全機制。ClassLoader的loadClass方法在加載類的時候使用了synchronized關鍵字。)
問題5 : 為什么提供靜態方法而不是直接將 INSTANCE 設置為 public?
通過向外提供公共方法, 體現了更好的封裝性, 可以在方法內實現懶加載的單例; 可以提供泛型等補充 : 任何一個readObject方法,不管是顯式的還是默認的,它都會返回一個新建的實例,這個新建的實例不同於該類初始化時創建的實例。
枚舉類實現餓漢式
枚舉的變量, 底層是通過public static final來修飾的, 類加載就創建了,所以是餓漢式
問題1:枚舉單例是如何限制實例個數的?
創建枚舉類的時候就已經定義好了,每個枚舉常量其實就是枚舉類的一個靜態成員變量
問題2:枚舉單例在創建時是否有並發問題?
沒有,枚舉單例底層是靜態成員變量,它是通過類加載器的加載而創建的, 確保了線程安全
問題3:枚舉單例能否被反射破壞單例?
不能
問題4:枚舉單例能否被反序列化破壞單例?
枚舉類默認實現了序列化接口,枚舉類已經考慮到此問題,無需擔心破壞單例
問題5:枚舉單例屬於懶漢式還是餓漢式?
餓漢式
問題6:枚舉單例如果希望加入一些單例創建時的初始化邏輯該如何做?
加構造方法就行了
public enum Singleton {
INSTANCE;
private Singleton() {
System.out.println("private Singleton2()");
}
@Override
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public static Singleton getInstance() {
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
小結
我們知道,類加載的方式是按需加載,且加載一次。因此,在上述單例類被加載時,就會實例化一個對象並交給自己的引用,供系統使用;而且,由於這個類在整個生命周期中只會被加載一次,因此只會創建一個實例,即能夠充分保證單例。
- 優點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。
- 缺點:在類裝載的時候就完成實例化,沒有達到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(),都要對對象上鎖,事實上,只有在第一次創建對象的時候需要加鎖,之后就不需要了,所以,這個地方需要改進。我們改成下面這個:
雙重鎖機制實現懶漢式
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兩個線程為例:
1>A、B線程同時進入了第一個if判斷
2>A首先進入synchronized塊,由於instance為null,所以它執行instance = new Singleton();
3>由於JVM內部的優化機制,JVM先畫出了一些分配給Singleton實例的空白內存,並賦值給instance成員(注意此時JVM沒有開始初始化這個實例),然后A離開了synchronized塊。
4>B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給調用該方法的程序。
5>此時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的內存初始化完畢,這樣我們就不用擔心上面的問題。同時該方法也只會在第一次調用的時候使用互斥機制,這樣就解決了低性能問題。這樣我們暫時總結一個完美的單例模式:
靜態內部類實現懶漢式
靜態內部類單例模式中實例由內部類創建,由於 JVM 在加載外部類的過程中, 是不會加載靜態內部類的, 只有內部類的屬性/方法被調用時才會被加載, 並初始化其靜態屬性。靜態屬性由於被 static
修飾,保證只被實例化一次,並且嚴格保證實例化順序。
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;
}
}
考慮性能的話,整個程序只需創建一次實例,所以性能也不會有什么影響。
小結
從這四種實現中,我們可以總結出,要想實現效率高的線程安全的單例,我們必須注意以下兩點:
- 盡量減少同步塊的作用域;
- 盡量使用細粒度的鎖。