【白話設計模式四】單例模式(Singleton)

#0 系列目錄#java

#1 場景問題# ##1.1 讀取配置文件的內容## 考慮這樣一個應用,讀取配置文件的內容。算法

不少應用項目,都有與應用相關的配置文件,這些配置文件可能是由項目開發人員自定義的,在裏面定義一些應用須要的參數數據。固然在實際的項目中,這種配置文件多采用xml格式的。也有采用properties格式的,畢竟使用Java來讀取properties格式的配置文件比較簡單。數據庫

如今要讀取配置文件的內容,該如何實現呢?編程

##1.2 不用模式的解決方案## 有些朋友會想,要讀取配置文件的內容,這也不是個什麼困難的事情,直接讀取文件的內容,而後把文件內容存放在相應的數據對象裏面就能夠了。真的這麼簡單嗎?先實現看看吧。設計模式

爲了示例簡單,假設系統是採用的properties格式的配置文件。緩存

  1. 那麼直接使用Java來讀取配置文件,示例代碼以下:
/**
 * 讀取應用配置文件
 */  
public class AppConfig {  
    /**
     * 用來存放配置文件中參數A的值
     */  
    private String parameterA;  
    /**
     * 用來存放配置文件中參數B的值
     */  
    private String parameterB;    

    public String getParameterA() {  
        return parameterA;  
    }  
    public String getParameterB() {  
        return parameterB;  
    }  
    /**
     * 構造方法
     */  
    public AppConfig(){  
        //調用讀取配置文件的方法  
        readConfig();  
    }  
    /**
     * 讀取配置文件,把配置文件中的內容讀出來設置到屬性上
     */  
    private void readConfig(){  
        Properties p = new Properties();  
        InputStream in = null;  
        try {  
            in = AppConfig.class.getResourceAsStream("AppConfig.properties");  
            p.load(in);  
            //把配置文件中的內容讀出來設置到屬性上  
            this.parameterA = p.getProperty("paramA");  
            this.parameterB = p.getProperty("paramB");  
        } catch (IOException e) {  
            System.out.println("裝載配置文件出錯了,具體堆棧信息以下:");  
            e.printStackTrace();  
        } finally {  
            try {  
                in.close();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}

注意:只有訪問參數的方法,沒有設置參數的方法。安全

  1. 應用的配置文件,名字是AppConfig.properties,放在AppConfig相同的包裏面,簡單示例以下:
paramA=a  
paramB=b
  1. 寫個客戶端來測試一下,示例代碼以下:
public class Client {  
    public static void main(String[] args) {  
        //建立讀取應用配置的對象  
        AppConfig config = new AppConfig();  
        
        String paramA = config.getParameterA();  
        String paramB = config.getParameterB();  
        
        System.out.println("paramA="+paramA+",paramB="+paramB);  
    }  
}

##1.3 有何問題## 上面的實現很簡單嘛,很容易的就實現了要求的功能。仔細想一想,有沒有什麼問題呢?多線程

看看客戶端使用這個類的地方,是經過new一個AppConfig的實例來獲得一個操做配置文件內容的對象。若是在系統運行中,有不少地方都須要使用配置文件的內容,也就是不少地方都須要建立AppConfig這個對象的實例。併發

換句話說,在系統運行期間,系統中會存在不少個AppConfig的實例對象,這有什麼問題嗎?框架

固然有問題了,試想一下,每個AppConfig實例對象,裏面都封裝着配置文件的內容,系統中有多個AppConfig實例對象,也就是說系統中會同時存在多份配置文件的內容,這會嚴重浪費內存資源。若是配置文件內容較少,問題還小一點,若是配置文件內容原本就多的話,對於系統資源的浪費問題就大了。事實上,對於AppConfig這種類,在運行期間,只須要一個實例對象就夠了

把上面的描述進一步抽象一下,問題就出來了:在一個系統運行期間,某個類只須要一個類實例就能夠了,那麼應該怎麼實現呢?

#2 解決方案# ##2.1 單例模式來解決## 用來解決上述問題的一個合理的解決方案就是單例模式。那麼什麼是單例模式呢?

  1. 單例模式定義

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

  1. 應用單例模式來解決的思路

仔細分析上面的問題,如今一個類可以被建立多個實例,問題的根源在於類的構造方法是公開的,也就是可讓類的外部來經過構造方法建立多個實例。換句話說,只要類的構造方法能讓類的外部訪問,就沒有辦法去控制外部來建立這個類的實例個數。

要想控制一個類只被建立一個實例,那麼首要的問題就是要把建立實例的權限收回來,讓類自身來負責本身類實例的建立工做,而後由這個類來提供外部能夠訪問這個類實例的方法,這就是單例模式的實現方式

##2.2 模式結構和說明## 單例模式結構如圖所示:

輸入圖片說明

Singleton:負責建立Singleton類本身的惟一實例,並提供一個getInstance的方法,讓外部來訪問這個類的惟一實例。

##2.3 單例模式示例代碼## 在Java中,單例模式的實現又分爲兩種,一種稱爲懶漢式,一種稱爲餓漢式,其實就是在具體建立對象實例的處理上,有不一樣的實現方式。下面分別來看這兩種實現方式的代碼示例。爲什麼這麼寫,具體的在後面再講述。

  1. 懶漢式實現,示例代碼以下:
/**
 * 懶漢式單例實現的示例
*/  
public class Singleton {  
   /**
    * 定義一個變量來存儲建立好的類實例
    */  
   private static Singleton uniqueInstance = null;  
   /**
    * 私有化構造方法,好在內部控制建立實例的數目
    */  
   private Singleton(){  
       //  
   }  
   /**
    * 定義一個方法來爲客戶端提供類實例
    * @return 一個Singleton的實例
    */  
   public static synchronized Singleton getInstance(){  
       //判斷存儲實例的變量是否有值  
       if(uniqueInstance == null){  
           //若是沒有,就建立一個類實例,並把值賦值給存儲類實例的變量  
           uniqueInstance = new Singleton();  
       }  
       //若是有值,那就直接使用  
       return uniqueInstance;  
   }  
   /**
    * 示意方法,單例能夠有本身的操做
    */  
   public void singletonOperation(){  
       //功能處理  
   }  
   /**
    * 示意屬性,單例能夠有本身的屬性
    */  
   private String singletonData;  
   /**
    * 示意方法,讓外部經過這些方法來訪問屬性的值
    * @return 屬性的值
    */  
   public String getSingletonData(){  
       return singletonData;  
   }  
}
  1. 餓漢式實現,示例代碼以下:
/**
 * 餓漢式單例實現的示例
 */  
public class Singleton {  
    /**
     * 定義一個變量來存儲建立好的類實例,直接在這裏建立類實例,只會建立一次
     */  
    private static Singleton uniqueInstance = new Singleton();  
    /**
     * 私有化構造方法,好在內部控制建立實例的數目
     */  
    private Singleton(){  
        //  
    }  
    /**
     * 定義一個方法來爲客戶端提供類實例
     * @return 一個Singleton的實例
     */  
    public static Singleton getInstance(){  
        //直接使用已經建立好的實例  
        return uniqueInstance;  
    }  
    
    /**
     * 示意方法,單例能夠有本身的操做
     */  
    public void singletonOperation(){  
        //功能處理  
    }  
    /**
     * 示意屬性,單例能夠有本身的屬性
     */  
    private String singletonData;  
    /**
     * 示意方法,讓外部經過這些方法來訪問屬性的值
     * @return 屬性的值
     */  
    public String getSingletonData(){  
        return singletonData;  
    }  
}

##2.4 使用單例模式重寫示例## 要使用單例模式來重寫示例,因爲單例模式有兩種實現方式,這裏選一種來實現就行了,就選擇餓漢式的實現方式來重寫示例吧。採用餓漢式的實現方式來重寫實例的示例代碼以下:

/**
 * 讀取應用配置文件,單例實現
 */  
public class AppConfig {  
    /**
     * 定義一個變量來存儲建立好的類實例,直接在這裏建立類實例,只會建立一次
     */  
    private static AppConfig instance = new AppConfig();  
    /**
     * 定義一個方法來爲客戶端提供AppConfig類的實例
     * @return 一個AppConfig的實例
     */  
    public static AppConfig getInstance(){  
        return instance;  
    }  
     
    /**
     * 用來存放配置文件中參數A的值
     */  
    private String parameterA;  
    /**
     * 用來存放配置文件中參數B的值
     */  
    private String parameterB;  
    public String getParameterA() {  
        return parameterA;  
    }  
    public String getParameterB() {  
        return parameterB;  
    }  
    /**
     * 私有化構造方法
     */  
    private AppConfig(){  
        //調用讀取配置文件的方法  
        readConfig();  
    }  
    /**
     * 讀取配置文件,把配置文件中的內容讀出來設置到屬性上
     */  
    private void readConfig(){  
        Properties p = new Properties();  
        InputStream in = null;  
        try {  
            in = AppConfig.class.getResourceAsStream("AppConfig.properties");  
            p.load(in);  
            //把配置文件中的內容讀出來設置到屬性上  
            this.parameterA = p.getProperty("paramA");  
            this.parameterB = p.getProperty("paramB");  
        } catch (IOException e) {  
            System.out.println("裝載配置文件出錯了,具體堆棧信息以下:");  
            e.printStackTrace();  
        } finally {  
            try {  
                in.close();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }    
}

固然,測試的客戶端也須要相應的變化,示例代碼以下:

public class Client {  
    public static void main(String[] args) {  
        //建立讀取應用配置的對象  
        AppConfig config = AppConfig.getInstance();  
 
        String paramA = config.getParameterA();  
        String paramB = config.getParameterB();  
 
        System.out.println("paramA="+paramA+",paramB="+paramB);  
    }
}

#3 模式講解# ##3.1 認識單例模式##

  1. 單例模式的功能

單例模式的功能是用來保證這個類在運行期間只會被建立一個類實例,另外單例模式還提供了一個全局惟一訪問這個類實例的訪問點,就是那個getInstance的方法。無論採用懶漢式仍是餓漢式的實現方式,這個全局訪問點是同樣的。

對於單例模式而言,無論採用何種實現方式,它都是隻關心類實例的建立問題,並不關心具體的業務功能。

  1. 單例模式的範圍

也就是在多大範圍內是單例呢?

觀察上面的實現能夠知道,目前Java裏面實現的單例是一個ClassLoader及其子ClassLoader的範圍。由於一個ClassLoader在裝載餓漢式實現的單例類的時候就會建立一個類的實例

這就意味着若是一個虛擬機裏面有不少個ClassLoader,並且這些ClassLoader都裝載某個類的話,就算這個類是單例,它也會產生不少個實例。固然,若是一個機器上有多個虛擬機,那麼每一個虛擬機裏面都應該至少有一個這個類的實例,也就是說整個機器上就有不少個實例,更不會是單例了。

另外請注意一點,這裏討論的單例模式並不適用於集羣環境,對於集羣環境下的單例這裏不去討論,那不屬於這裏的內容範圍。

  1. 單例模式的命名

通常建議單例模式的方法命名爲:getInstance(),這個方法的返回類型確定是單例類的類型了。getInstance方法能夠有參數,這些參數多是建立類實例所須要的參數,固然,大多數狀況下是不須要的。

單例模式的名稱:單例、單件、單體等等,翻譯的不一樣,都是指的同一個模式。

##3.2 懶漢式和餓漢式實現## 前面提到了單例模式有兩種典型的解決方案,一種叫懶漢式,一種叫餓漢式,這兩種方式到底是如何實現的,下面分別來看看。爲了看得更清晰一點,只是實現基本的單例控制部分,再也不提供示例的屬性和方法了;並且暫時也不去考慮線程安全的問題,這個問題在後面會重點分析

第一種方案 懶漢式

  1. 私有化構造方法:

要想在運行期間控制某一個類的實例只有一個,那首先的任務就是要控制建立實例的地方,也就是不能隨隨便便就能夠建立類實例,不然就沒法控制建立的實例個數了。如今是讓使用類的地方來建立類實例,也就是在類外部來建立類實例。那麼怎樣才能讓類的外部不能建立一個類的實例呢?很簡單,私有化構造方法就能夠了!

private Singleton() {  
}
  1. 提供獲取實例的方法

構造方法被私有化了,外部使用這個類的地方不幹了,外部建立不了類實例就沒有辦法調用這個對象的方法,就實現不了功能處理,這可不行。通過思考,單例模式決定讓這個類提供一個方法來返回類的實例,好讓外面使用。示例代碼以下:

public Singleton getInstance() {  
}
  1. 把獲取實例的方法變成靜態的

又有新的問題了,獲取對象實例的這個方法是個實例方法,也就是說客戶端要想調用這個方法,須要先獲得類實例,而後才能夠調用,但是這個方法就是爲了獲得類實例,這樣一來不就造成一個死循環了嗎?這不就是典型的「先有雞仍是先有蛋的問題」嘛

解決方法也很簡單,在方法上加上static,這樣就能夠直接經過類來調用這個方法,而不須要先獲得類實例了,示例代碼以下:

public static Singleton getInstance() {  
}
  1. 定義存儲實例的屬性

方法定義好了,那麼方法內部如何實現呢?若是直接建立實例並返回,這樣行不行呢?示例代碼以下:

public static Singleton getInstance(){  
    return new Singleton();  
}

固然不行了,若是每次客戶端訪問都這樣直接new一個實例,那確定會有多個實例,根本實現不了單例的功能。

怎麼辦呢?單例模式想到了一個辦法,那就是用一個屬性來記錄本身建立好的類實例,當第一次建立事後,就把這個實例保存下來,之後就能夠複用這個實例,而不是重複建立對象實例了。示例代碼以下:

private Singleton instance = null;
  1. 把這個屬性也定義成靜態的

這個屬性變量應該在什麼地方用呢?確定是第一次建立類實例的地方,也就是在前面那個返回對象實例的靜態方法裏面使用。

因爲要在一個靜態方法裏面使用,因此這個屬性被迫成爲一個類變量,要強制加上static,也就是說,這裏並無使用static的特性。示例代碼以下:

private static Singleton instance = null;
  1. 實現控制實例的建立

如今應該到getInstance方法裏面實現控制實例建立了,控制的方式很簡單,只要先判斷一下,是否已經建立過實例了。如何判斷?那就看存放實例的屬性是否有值,若是有值,說明已經建立過了,若是沒有值,那就是應該建立一個,示例代碼以下:

public static Singleton getInstance() {  
    //先判斷instance是否有值  
    if (instance == null) {  
        //若是沒有值,說明尚未建立過實例,那就建立一個  
        //並把這個實例設置給instance  
        instance = new Singleton ();  
    }  
    //若是有值,或者是建立了值,那就直接使用  
    return instance;  
}
  1. 完整的實現

至此,成功解決了:在運行期間,控制某個類只被建立一個實例的要求。完整的代碼以下,爲了你們好理解,用註釋標示了代碼的前後順序,示例代碼以下:

public class Singleton {  
    //4:定義一個變量來存儲建立好的類實例  
    //5:由於這個變量要在靜態方法中使用,因此須要加上static修飾  
    private static Singleton instance = null;  
    //1:私有化構造方法,好在內部控制建立實例的數目  
    private Singleton(){      
    }  
    //2:定義一個方法來爲客戶端提供類實例  
    //3:這個方法須要定義成類方法,也就是要加static  
    public static Singleton getInstance(){  
        //6:判斷存儲實例的變量是否有值  
        if(instance == null){  
            //6.1:若是沒有,就建立一個類實例,並把值賦值給存儲類實例的變量  
            instance = new Singleton();  
        }  
        //6.2:若是有值,那就直接使用  
        return instance;  
    }  
}

第二種方案 餓漢式

這種方案跟第一種方案相比,前面的私有化構造方法,提供靜態的getInstance方法來返回實例等步驟都同樣。差異在如何實現getInstance方法,在這個地方,單例模式還想到了另一種方法來實現getInstance方法。

不就是要控制只創造一個實例嗎?那麼有沒有什麼現成的解決辦法呢?很快,單例模式回憶起了Java中static的特性:

static變量在類裝載的時候進行初始化。

多個實例的static變量會共享同一塊內存區域。

這就意味着,在Java中,static變量只會被初始化一次,就是在類裝載的時候,並且多個實例都會共享這個內存空間,這不就是單例模式要實現的功能嗎?真是得來全不費功夫啊。根據這些知識,寫出了第二種解決方案的代碼,示例代碼以下:

public class Singleton {  
    //4:定義一個靜態變量來存儲建立好的類實例  
    //直接在這裏建立類實例,只會建立一次  
    private static Singleton instance = new Singleton();  
    //1:私有化構造方法,好在內部控制建立實例的數目  
    private Singleton(){          
    }  
    //2:定義一個方法來爲客戶端提供類實例  
    //3:這個方法須要定義成類方法,也就是要加static  
    //這個方法裏面就不須要控制代碼了  
    public static Singleton getInstance(){  
        //5:直接使用已經建立好的實例  
        return instance;  
    }  
}

無論是採用哪種方式,在運行期間,都只會生成一個實例,而訪問這些類的一個全局訪問點,就是那個靜態的getInstance方法。

單例模式的調用順序示意圖

先看懶漢式的調用順序,如圖所示:

輸入圖片說明

餓漢式的調用順序,如圖所示:

輸入圖片說明

##3.3 延遲加載的思想## 單例模式的懶漢式實現方式體現了延遲加載的思想,什麼是延遲加載呢?

通俗點說,就是一開始不要加載資源或者數據,一直等,等到立刻就要使用這個資源或者數據了,躲不過去了才加載,因此也稱Lazy Load,不是懶惰啊,是「延遲加載」,這在實際開發中是一種很常見的思想,儘量的節約資源。

體如今什麼地方呢?看以下代碼:

輸入圖片說明

##3.4 緩存的思想## 單例模式的懶漢式實現還體現了緩存的思想,緩存也是實際開發中很是常見的功能。

簡單講就是,若是某些資源或者數據會被頻繁的使用,而這些資源或數據存儲在系統外部,好比數據庫、硬盤文件等,那麼每次操做這些數據的時候都從數據庫或者硬盤上去獲取,速度會很慢,會形成性能問題。

一個簡單的解決方法就是:把這些數據緩存到內存裏面,每次操做的時候,先到內存裏面找,看有沒有這些數據,若是有,那麼就直接使用,若是沒有那麼就獲取它,並設置到緩存中,下一次訪問的時候就能夠直接從內存中獲取了。從而節省大量的時間,固然,緩存是一種典型的空間換時間的方案。

緩存在單例模式的實現中怎麼體現的呢?

輸入圖片說明

##3.5 Java中緩存的基本實現## 引伸一下,看看在Java開發中的緩存的基本實現,在Java中最多見的一種實現緩存的方式就是使用Map,基本的步驟是:

先到緩存裏面查找,看看是否存在須要使用的數據

若是沒有找到,那麼就建立一個知足要求的數據,而後把這個數據設置回到緩存中,以備下次使用

若是找到了相應的數據,或者是建立了相應的數據,那就直接使用這個數據。

仍是看看示例吧,示例代碼以下:

/**
 * Java中緩存的基本實現示例
 */  
public class JavaCache {  
   /**
    * 緩存數據的容器,定義成Map是方便訪問,直接根據Key就能夠獲取Value了
    * key選用String是爲了簡單,方便演示
    */  
   private Map<String,Object> map = new HashMap<String,Object>();  
   /**
    * 從緩存中獲取值
    * @param key 設置時候的key值
    * @return key對應的Value值
    */  
   public Object getValue(String key){  
       //先從緩存裏面取值  
       Object obj = map.get(key);  
       //判斷緩存裏面是否有值  
       if(obj == null){  
           //若是沒有,那麼就去獲取相應的數據,好比讀取數據庫或者文件  
           //這裏只是演示,因此直接寫個假的值  
           obj = key+",value";  
           //把獲取的值設置回到緩存裏面  
           map.put(key, obj);  
       }  
       //若是有值了,就直接返回使用  
       return obj;  
   }  
}

這裏只是緩存的基本實現,還有不少功能都沒有考慮,好比緩存的清除,緩存的同步等等。固然,Java的緩存還有不少實現方式,也是很是複雜的,如今有不少專業的緩存框架,更多緩存的知識,這裏就再也不去討論了。

##3.6 利用緩存來實現單例模式## 其實應用Java緩存的知識,也能夠變相實現Singleton模式,算是一個模擬實現吧。每次都先從緩存中取值,只要建立一次對象實例事後,就設置了緩存的值,那麼下次就不用再建立了。

雖然不是很標準的作法,可是一樣能夠實現單例模式的功能,爲了簡單,先不去考慮多線程的問題,示例代碼以下:

/**
 * 使用緩存來模擬實現單例
 */  
public class Singleton {  
   /**
    * 定義一個缺省的key值,用來標識在緩存中的存放
    */  
   private final static String DEFAULT_KEY = "One";  
   /**
    * 緩存實例的容器
    */  
   private static Map<String,Singleton> map = new HashMap<String,Singleton>();  
   /**
    * 私有化構造方法
    */  
   private Singleton(){  
       //  
   }  
   public static Singleton getInstance(){  
       //先從緩存中獲取  
       Singleton instance = (Singleton)map.get(DEFAULT_KEY);  
       //若是沒有,就新建一個,而後設置回緩存中  
       if(instance==null){  
           instance = new Singleton();  
           map.put(DEFAULT_KEY, instance);  
       }  
       //若是有就直接使用  
       return instance;  
   }  
}

##3.7 單例模式的優缺點##

  1. 時間和空間

懶漢式是典型的時間換空間,也就是每次獲取實例都會進行判斷,看是否須要建立實例,費判斷的時間,固然,若是一直沒有人使用的話,那就不會建立實例,節約內存空間。

餓漢式是典型的空間換時間,當類裝載的時候就會建立類實例,無論你用不用,先建立出來,而後每次調用的時候,就不須要再判斷了,節省了運行時間。

  1. 線程安全

(1)從線程安全性上講,不加同步的懶漢式是線程不安全的,好比說:有兩個線程,一個是線程A,一個是線程B,它們同時調用getInstance方法,那就可能致使併發問題。以下示例:

輸入圖片說明

程序繼續運行,兩個線程都向前走了一步,以下:

輸入圖片說明

可能有些朋友會以爲文字描述仍是不夠直觀,再來畫個圖說明一下,如圖所示:

輸入圖片說明

經過上圖的分解描述,明顯能夠看出,當A、B線程併發的狀況下,會建立出兩個實例來,也就是單例的控制在併發狀況下失效了。

(2)餓漢式是線程安全的,由於虛擬機保證了只會裝載一次,在裝載類的時候是不會發生併發的。

(3)如何實現懶漢式的線程安全呢?固然懶漢式也是能夠實現線程安全的,只要加上synchronized便可,以下:

public static synchronized Singleton getInstance(){}

可是這樣一來,會下降整個訪問的速度,並且每次都要判斷,也確實是稍微慢點。那麼有沒有更好的方式來實現呢?

(4)雙重檢查加鎖,可使用「雙重檢查加鎖」的方式來實現,就能夠既實現線程安全,又可以使性能不受到大的影響。那麼什麼是「雙重檢查加鎖」機制呢?

所謂雙重檢查加鎖機制,指的是:並非每次進入getInstance方法都須要同步,而是先不一樣步,進入方法事後,先檢查實例是否存在,若是不存在才進入下面的同步塊,這是第一重檢查。進入同步塊事後,再次檢查實例是否存在,若是不存在,就在同步的狀況下建立一個實例,這是第二重檢查。這樣一來,就只須要同步一次了,從而減小了屢次在同步狀況下進行判斷所浪費的時間。

雙重檢查加鎖機制的實現會使用一個關鍵字volatile,它的意思是:被volatile修飾的變量的值,將不會被本地線程緩存,全部對該變量的讀寫都是直接操做共享內存,從而確保多個線程能正確的處理該變量。

注意:在Java1.4及之前版本中,不少JVM對於volatile關鍵字的實現有問題,會致使雙重檢查加鎖的失敗,所以雙重檢查加鎖的機制只能用在Java5及以上的版本。

看看代碼可能會更清楚些,示例代碼以下:

public class Singleton {  
    /**
     * 對保存實例的變量添加volatile的修飾
     */  
    private volatile static Singleton instance = null;  
    private Singleton(){      
    }  
    public static  Singleton getInstance(){  
        //先檢查實例是否存在,若是不存在才進入下面的同步塊  
        if(instance == null){  
            //同步塊,線程安全的建立實例  
            synchronized(Singleton.class){  
                //再次檢查實例是否存在,若是不存在才真的建立實例  
                if(instance == null){  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}

這種實現方式既可以使實現線程安全的建立實例,又不會對性能形成太大的影響,它只是在第一次建立實例的時候同步,之後就不須要同步了,從而加快運行速度。

提示:因爲volatile關鍵字可能會屏蔽掉虛擬機中一些必要的代碼優化,因此運行效率並非很高,所以通常建議,沒有特別的須要,不要使用。也就是說,雖然可使用雙重加鎖機制來實現線程安全的單例,但並不建議大量採用,根據狀況來選用吧。

##3.8 在Java中一種更好的單例實現方式## 根據上面的分析,常見的兩種單例實現方式都存在小小的缺陷,那麼有沒有一種方案,既可以實現延遲加載,又可以實現線程安全呢?

還真有高人想到這樣的解決方案了,這個解決方案被稱爲Lazy initialization holder class模式,這個模式綜合使用了Java的類級內部類和多線程缺省同步鎖的知識,很巧妙的同時實現了延遲加載和線程安全

  1. 先來看點相應的基礎知識

什麼是類級內部類?簡單點說,類級內部類指的是:有static修飾的成員式內部類。若是沒有static修飾的成員式內部類被稱爲對象級內部類

類級內部類至關於其外部類的static成分,它的對象與外部類對象間不存在依賴關係,所以可直接建立。而對象級內部類的實例,是綁定在外部對象實例中的。

類級內部類中,能夠定義靜態的方法,在靜態方法中只可以引用外部類中的靜態成員方法或者成員變量。

類級內部類至關於其外部類的成員,只有在第一次被使用的時候纔會被裝載。

再來看看多線程缺省同步鎖的知識。

你們都知道,在多線程開發中,爲了解決併發問題,主要是經過使用synchronized來加互斥鎖進行同步控制。可是在某些狀況中,JVM已經隱含地爲您執行了同步,這些狀況下就不用本身再來進行同步控制了。這些狀況包括:

由靜態初始化器(在靜態字段上或 static{} 塊中的初始化器)初始化數據時

訪問 final 字段時

在建立線程以前建立對象時

線程能夠看見它將要處理的對象時

  1. 接下來看看這種解決方案的思路

要想很簡單的實現線程安全,能夠採用靜態初始化器的方式,它能夠由JVM來保證線程安全性。好比前面的「餓漢式」實現方式,可是這樣一來,不是會浪費必定的空間嗎?由於這種實現方式,會在類裝載的時候就初始化對象,無論你需不須要。

若是如今有一種方法可以讓類裝載的時候不去初始化對象,那不就解決問題了?一種可行的方式就是採用類級內部類,在這個類級內部類裏面去建立對象實例,這樣一來,只要不使用到這個類級內部類,那就不會建立對象實例。從而同時實現延遲加載和線程安全。

看看代碼示例可能會更清晰,示例代碼以下:

public class Singleton {  
    /**
     * 類級的內部類,也就是靜態的成員式內部類,該內部類的實例與外部類的實例
     * 沒有綁定關係,並且只有被調用到纔會裝載,從而實現了延遲加載
     */  
    private static class SingletonHolder {  
        /**
         * 靜態初始化器,由JVM來保證線程安全
         */  
        private static Singleton instance = new Singleton();  
    }  
    /**
     * 私有化構造方法
     */  
    private Singleton() {  
    }  
    public static  Singleton getInstance() {  
        return SingletonHolder.instance;  
    }  
}

仔細想一想,是否是很巧妙呢!

當getInstance方法第一次被調用的時候,它第一次讀取SingletonHolder.instance,致使SingletonHolder類獲得初始化;而這個類在裝載並被初始化的時候,會初始化它的靜態域,從而建立Singleton的實例,因爲是靜態的域,所以只會被虛擬機在裝載類的時候初始化一次,並由虛擬機來保證它的線程安全性。

這個模式的優點在於,getInstance方法並無被同步,而且只是執行一個域的訪問,所以延遲初始化並無增長任何訪問成本。

##3.9 單例和枚舉## 按照《高效Java 第二版》中的說法:單元素的枚舉類型已經成爲實現Singleton的最佳方法

爲了理解這個觀點,先來了解一點相關的枚舉知識,這裏只是強化和總結一下枚舉的一些重要觀點,更多基本的枚舉的使用,請參看Java編程入門資料:

Java的枚舉類型實質上是功能齊全的類,所以能夠有本身的屬性和方法

Java枚舉類型的基本思想:經過公有的靜態final域爲每一個枚舉常量導出實例的類

從某個角度講,枚舉是單例的泛型化,本質上是單元素的枚舉

用枚舉來實現單例很是簡單,只須要編寫一個包含單個元素的枚舉類型便可,示例代碼以下:

/**
 * 使用枚舉來實現單例模式的示例
 */  
public enum Singleton {  
    /**
     * 定義一個枚舉的元素,它就表明了Singleton的一個實例
     */  
    uniqueInstance;  
     
    /**
     * 示意方法,單例能夠有本身的操做
     */  
    public void singletonOperation(){  
        //功能處理  
    }  
}

使用枚舉來實現單實例控制,會更加簡潔,並且無償的提供了序列化的機制,並由JVM從根本上提供保障,絕對防止屢次實例化,是更簡潔、高效、安全的實現單例的方式。

##3.10 思考單例模式##

  1. 單例模式的本質

單例模式的本質:控制實例數目。

單例模式是爲了控制在運行期間,某些類的實例數目只能有一個。可能有人就會想了,那麼我能不能控制實例數目爲2個,3個,或者是任意多個呢?目的都是同樣的,節省資源啊,有些時候單個實例不能知足實際的須要,會忙不過來,根據測算,3個實例剛恰好,也就是說,如今要控制實例數目爲3個,怎麼辦呢?

其實思路很簡單,就是利用上面經過Map來緩存實現單例的示例,進行變形,一個Map能夠緩存任意多個實例,新的問題就是,Map中有多個實例,可是客戶端調用的時候,到底返回那一個實例呢,也就是實例的調度問題,咱們只是想要來展現設計模式,對於這個調度算法就不去深究了,作個最簡單的,循環返回就行了,示例代碼以下:

/**
 * 簡單演示如何擴展單例模式,控制實例數目爲3個
 */  
public class OneExtend {  
    /**
     * 定義一個缺省的key值的前綴
     */  
    private final static String DEFAULT_PREKEY = "Cache";  
    /**
     * 緩存實例的容器
     */  
    private static Map<String,OneExtend> map = new HashMap<String,OneExtend>();  
    /**
     * 用來記錄當前正在使用第幾個實例,到了控制的最大數目,就返回從1開始
     */  
    private static int num = 1;  
    /**
     * 定義控制實例的最大數目
     */  
    private final static int NUM_MAX = 3;  
    private OneExtend(){}  
    public static OneExtend getInstance(){  
        String key = DEFAULT_PREKEY+num;  
        //緩存的體現,經過控制緩存的數據多少來控制實例數目  
        OneExtend oneExtend = map.get(key);  
        if(oneExtend==null){  
            oneExtend = new OneExtend();  
            map.put(key, oneExtend);  
        }  
        //把當前實例的序號加1  
        num++;  
        if(num > NUM_MAX){  
            //若是實例的序號已經達到最大數目了,那就重複從1開始獲取  
            num = 1;  
        }  
        return oneExtend;        
    }  
    
    public static void main(String[] args) {  
        //測試是否能知足功能要求  
        OneExtend t1 = getInstance ();  
        OneExtend t2 = getInstance ();  
        OneExtend t3 = getInstance ();  
        OneExtend t4 = getInstance ();  
        OneExtend t5 = getInstance ();  
        OneExtend t6 = getInstance ();  
        
        System.out.println("t1=="+t1);  
        System.out.println("t2=="+t2);  
        System.out.println("t3=="+t3);  
        System.out.println("t4=="+t4);  
        System.out.println("t5=="+t5);  
        System.out.println("t6=="+t6);  
    }  
}

測試一下,看看結果,以下:

t1==cn.javass.dp.singleton.example9.OneExtend@6b97fd  
t2==cn.javass.dp.singleton.example9.OneExtend@1c78e57  
t3==cn.javass.dp.singleton.example9.OneExtend@5224ee  
t4==cn.javass.dp.singleton.example9.OneExtend@6b97fd  
t5==cn.javass.dp.singleton.example9.OneExtend@1c78e57  
t6==cn.javass.dp.singleton.example9.OneExtend@5224ee
  1. 什麼時候選用單例模式

建議在以下狀況中,選用單例模式:

當須要控制一個類的實例只能有一個,並且客戶只能從一個全局訪問點訪問它時,能夠選用單例模式,這些功能剛好是單例模式要解決的問題。

相關文章
相關標籤/搜索