1. 程式人生 > >內部類的單例模式

內部類的單例模式

* 上面的單例實現存在小小的缺陷,那麼 有沒有一種方法,既能夠實現延遲載入,又能夠
 * 實現執行緒安全呢?
 *       還真有高人想到這樣的解決方案了,這個解決方案被稱為Lazy initialization
 * holder class 模式,這個模式綜合使用了java的類級內部類和多執行緒預設同步鎖的知識,
 * 很巧妙的同時實現了延遲載入和執行緒安全。
 *
 *
 * 1 相應的基礎知識
 *  (1)什麼是類級內部類?
 *   簡單點說,類級內部類指的是,有static修飾的成員內部類。如果沒有static修飾的成員式內
 *   部類被稱為物件級內部類。
 *   (2)類級內部類相當於其外部類的static成分,它的物件與外部類物件間不存在依賴關係,因此
 *   可以直接建立。而物件級內部類的例項,是繫結在外部物件例項中的。
 *   (3)類級內部類中,可以定義靜態的方法。在靜態方法中只能引用外部類中的靜態成員方法或變數。
 *   (4)類級內部類相當於其外部類的成員,只有在第一次被使用的時候才會被裝載。
 *  
 *   多執行緒預設同步鎖的知識:
 *   大家都知道,在多執行緒開發中,為了解決併發問題,主要是通過使用synchronized來加互斥鎖進行同步控制,
 *   但是在某些情況下,JVM已經隱含的為您執行了同步,這些情況下就不用自己再來進行同步控制了。
 *   這些情況包括:
 *   (1)由靜態初始化器(在靜態欄位上或static{}塊中的初始化器)初始化資料時
 *   (2)訪問final欄位時
 *   (3)在建立執行緒之前建立物件時
 *   (4)執行緒可以看見它將要處理的物件時
 *  
 *  
 *   2 解決方案的思路
 *        要想很簡單的實現執行緒安全,可以採用靜態初始化器的方式,它可以由JVM來保證執行緒的
 *   安全性。比如前面的餓漢式實現方式。但是這樣一來,不是會浪費一定的空間嗎?因為這種
 *   實現方式,會在類裝載的時候就初始化物件,不管你需不需要。
 *        如果現在有一種方法能夠讓類裝載的時候不去初始化物件,那不就解決問題了?一種可行的
 *   方式就是採用類級內部類,在這個類級內部類裡面去建立物件例項。這樣一來,只要不使用到這個類級內部類,
 *   那就不會建立物件例項,從而同步實現延遲載入和執行緒安全。

public class Singleton_InnerClass {
        
 private static class SingletonHolder{
  //靜態初始化器,由JVM來保證執行緒安全
  private static Singleton_InnerClass instance=new Singleton_InnerClass();
 }
 
 //私有化構造方法
 private Singleton_InnerClass(){
  
 }
 
 public static Singleton_InnerClass getInstance(){
  return SingletonHolder.instance;
 }
}

===========================================================================

最近在看何紅輝、關愛民著的《Android原始碼設計模式解析與實戰》,一邊學習,一邊理解,一邊記筆記。

1.定義

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

2.應用場景

  1. 當產生多個物件會消耗過多資源,比如IO和資料操作
  2. 某種型別的物件只應該有且只有一個,比如Android中的Application。

3.考慮情況

  1. 多執行緒造成例項不唯一。
  2. 反序列化過程生成了新的例項。

4.實現方式

4.1普通單例模式

/**
 * 普通模式
 * @author
josan_tang */
public class SimpleSingleton { //1.static單例變數 private static SimpleSingleton instance; //2.私有的構造方法 private SimpleSingleton() { } //3.靜態方法為呼叫者提供單例物件 public static SimpleSingleton getInstance() { if (instance == null) { instance = new SimpleSingleton(); } return instance; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在多執行緒高併發的情況下,這樣寫會有明顯的問題,當執行緒A呼叫getInstance方法,執行到16行時,檢測到instance為null,於是執行17行去例項化instance,當17行沒有執行完時,執行緒B又呼叫了getInstance方法,這時候檢測到instance依然為空,所以執行緒B也會執行17行去建立一個新的例項。這時候,執行緒A和執行緒B得到的instance就不是一個了,這違反了單例的定義。

4.2 餓漢單例模式

/**
 * 餓漢單例模式
 * @author josan_tang
 */
public class EHanSingleton {
    //static final單例物件,類載入的時候就初始化
    private static final EHanSingleton instance = new EHanSingleton();

    //私有構造方法,使得外界不能直接new
    private EHanSingleton() {
    }

    //公有靜態方法,對外提供獲取單例介面
    public static EHanSingleton getInstance() {
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

餓漢單例模式解決了多執行緒併發的問題,因為在載入這個類的時候,就例項化了instance。當getInstatnce方法被呼叫時,得到的永遠是類載入時初始化的物件(反序列化的情況除外)。但這也帶來了另一個問題,如果有大量的類都採用了餓漢單例模式,那麼在類載入的階段,會初始化很多暫時還沒有用到的物件,這樣肯定會浪費記憶體,影響效能,我們還是要傾向於4.1的做法,在首次呼叫getInstance方法時才初始化instance。請繼續看4.3用法。

4.3懶漢單例模式

import java.io.Serializable;

/**
 * 懶漢模式
 * @author josan_tang
 */
public class LanHanSingleton {
    private static LanHanSingleton instance;

    private LanHanSingleton() {

    }

    /**
     * 增加synchronized關鍵字,該方法為同步方法,保證多執行緒單例物件唯一
     */
    public static synchronized LanHanSingleton getInstance() {
        if (instance == null) {
            instance = new LanHanSingleton();
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

與4.1的唯一區別在於getInstance方法前加了synchronized 關鍵字,讓getInstance方法成為同步方法,這樣就保證了當getInstance第一次被呼叫,即instance被例項化時,別的呼叫不會進入到該方法,保證了多執行緒中單例物件的唯一性。

優點:單例物件在第一次呼叫才被例項化,有效節省記憶體,並保證了執行緒安全。

缺點:同步是針對方法的,以後每次呼叫getInstance時(就算intance已經被例項化了),也會進行同步,造成了不必要的同步開銷。不推薦使用這種方式。

4.4 Double CheckLock(DCL)單例模式

/**
 * Double CheckLock(DCL)模式
 * @author josan_tang
 *
 */
public class DCLSingleton {
    //增加volatile關鍵字,確保例項化instance時,編譯成彙編指令的執行順序
    private volatile static DCLSingleton instance;

    private DCLSingleton() {

    }

    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                //當第一次呼叫getInstance方法時,即instance為空時,同步操作,保證多執行緒例項唯一
                //當以後呼叫getInstance方法時,即instance不為空時,不進入同步程式碼塊,減少了不必要的同步開銷
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

DCL失效:

在JDK1.5之前,可能會有DCL實現的問題,上述程式碼中的20行,在Java裡雖然是一句程式碼,但它並不是一個真正的原子操作。

instance = new DCLSingleton();
  • 1
  • 1

它編譯成最終的彙編指令,會有下面3個階段:

  1. 給DCLSingleton例項分配記憶體
  2. 呼叫DCLSingleton的建構函式,初始化成員變數。
  3. 將instance指向分配的記憶體空間(這個操作以後,instance才不為null)

在jdk1.5之前,上述的2、3步驟不能保證順序,也就是說有可能是1-2-3,也有可能是1-3-2。如果是1-3-2,當執行緒A執行完步驟3(instance已經不為null),但是還沒執行完2,執行緒B又呼叫了getInstance方法,這時候執行緒B所得到的就是執行緒A沒有執行步驟2(沒有執行完建構函式)的instance,執行緒B在使用這樣的instance時,就有可能會出錯。這就是DCL失效。

在jdk1.5之後,可以使用volatile關鍵字,保證彙編指令的執行順序,雖然會影響效能,但是和程式的正確性比起來,可以忽悠不計。

Java記憶體模型

優點:第一次執行getInstance時instance才被例項化,節省記憶體;多執行緒情況下,基本安全;並且在instance例項化以後,再次呼叫getInstance時,不會有同步消耗。

缺點:jdk1.5以下,有可能DCL失效;Java記憶體模型影響導致失效;jdk1.5以後,使用volatile關鍵字,雖然能解決DCL失效問題,但是會影響部分效能。

4.5 靜態內部類單例模式

/**
 * 靜態內部類實現單例模式
 * @author josan_tang
 *
 */
public class StaticClassSingleton {
    //私有的構造方法,防止new
    private StaticClassSingleton() {

    }

    private static StaticClassSingleton getInstance() {
        return StaticClassSingletonHolder.instance;
    }

    /**
     * 靜態內部類
     */
    private static class StaticClassSingletonHolder {
        //第一次載入內部類的時候,例項化單例物件
        private static final StaticClassSingleton instance = new StaticClassSingleton();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

第一次載入StaticClassSingleton類時,並不會例項化instance,只有第一次呼叫getInstance方法時,Java虛擬機器才會去載入StaticClassSingletonHolder類,繼而例項化instance,這樣延時例項化instance,節省了記憶體,並且也是執行緒安全的。這是推薦使用的一種單例模式。

4.6 列舉單例模式

/**
 * 列舉單例模式
 * @author josan_tang
 *
 */
public enum EnumSingleton {
    //列舉例項的建立是執行緒安全的,任何情況下都是單例(包括反序列化)
    INSTANCE;

    public void doSomething(){

    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

列舉不僅有欄位還能有自己的方法,並且列舉例項建立是執行緒安全的,就算反序列化時,也不會建立新的例項。除了列舉模式以外,其他實現方式,在反序列化時都會建立新的物件。

為了防止物件在反序列化時建立新的物件,需要加上如下方法:

    private Object readResole() throws ObjectStreamException {
        return instance;
    }
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

這是一個鉤子函式,在反序列化建立物件時會呼叫它,我們直接返回instance就是說,不要按照預設那樣去建立新的物件,而是直接將instance返回。

4.7 容器單例模式

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

/**
 * 容器單例模式
 * @author josan_tang
 */
public class ContainerSingleton {
    private static Map<String, Object> singletonMap = new HashMap<String, Object>();

    //單例物件加入到集合,key要保證唯一性
    public static void putSingleton(String key, Object singleton){
        if (key != null && !"".equals(key) && singleton != null) {
            if (!singletonMap.containsKey(key)) {   //這樣防止集合裡有一個類的兩個不同物件
                singletonMap.put(key, singleton);   
            }
        }
    }

    //根據key從集合中得到單例物件
    public static Object getSingleton(String key) {
        return singletonMap.get(key);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

在程式初始化的時候,講多種單例型別物件加入到一個單例集合裡,統一管理。在使用時,通過key從集合中獲取單例物件。這種方式多見於系統中的單例,像安卓中的系統級別服務就是採用集合形式的單例模式,比如常用的LayoutInfalter,我們一般在Fragment中的getView方法中使用如下程式碼:

View view = LayoutInflater.from(context).inflate(R.layout.xxx, null);
  • 1
  • 1

其實LayoutInflater.from(context)就是得到LayoutInflater例項,看下面的Android原始碼:

    /**
     * Obtains the LayoutInflater from the given context.
     */
    public static LayoutInflater from(Context context) {
        //通過key,得到LayoutInflater例項
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

總結

不管是那種形式實現單例模式,其核心思想都是將構造方法私有化,並且通過靜態方法獲取一個唯一的例項。在獲取過程中需要保證執行緒安全、防止反序列化導致重新生成例項物件。選擇哪種實現方式看情況而定,比如是否高併發、JDK版本、單例物件的資源消耗等。