* 上面的單例實作存在小小的缺陷,那麼 有沒有一種方法,既能夠實作延遲加載,又能夠
* 實作線程安全呢?
* 還真有高人想到這樣的解決方案了,這個解決方案被稱為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.應用場景
- 當産生多個對象會消耗過多資源,比如IO和資料操作
- 某種類型的對象隻應該有且隻有一個,比如Android中的Application。
3.考慮情況
- 多線程造成執行個體不唯一。
- 反序列化過程生成了新的執行個體。
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;
}
}
在多線程高并發的情況下,這樣寫會有明顯的問題,當線程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;
}
}
餓漢單例模式解決了多線程并發的問題,因為在加載這個類的時候,就執行個體化了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;
}
}
與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;
}
}
DCL失效:
在JDK1.5之前,可能會有DCL實作的問題,上述代碼中的20行,在Java裡雖然是一句代碼,但它并不是一個真正的原子操作。
instance = new DCLSingleton();
它編譯成最終的彙編指令,會有下面3個階段:
- 給DCLSingleton執行個體配置設定記憶體
- 調用DCLSingleton的構造函數,初始化成員變量。
- 将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();
}
}
第一次加載StaticClassSingleton類時,并不會執行個體化instance,隻有第一次調用getInstance方法時,Java虛拟機才會去加載StaticClassSingletonHolder類,繼而執行個體化instance,這樣延時執行個體化instance,節省了記憶體,并且也是線程安全的。這是推薦使用的一種單例模式。
4.6 枚舉單例模式
/**
* 枚舉單例模式
* @author josan_tang
*
*/
public enum EnumSingleton {
//枚舉執行個體的建立是線程安全的,任何情況下都是單例(包括反序列化)
INSTANCE;
public void doSomething(){
}
}
枚舉不僅有字段還能有自己的方法,并且枚舉執行個體建立是線程安全的,就算反序列化時,也不會建立新的執行個體。除了枚舉模式以外,其他實作方式,在反序列化時都會建立新的對象。
為了防止對象在反序列化時建立新的對象,需要加上如下方法:
private Object readResole() throws ObjectStreamException {
return instance;
}
這是一個鈎子函數,在反序列化建立對象時會調用它,我們直接傳回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);
}
}
在程式初始化的時候,講多種單例類型對象加入到一個單例集合裡,統一管理。在使用時,通過key從集合中擷取單例對象。這種方式多見于系統中的單例,像安卓中的系統級别服務就是采用集合形式的單例模式,比如常用的LayoutInfalter,我們一般在Fragment中的getView方法中使用如下代碼:
View view = LayoutInflater.from(context).inflate(R.layout.xxx, null);
其實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;
}