1.情境類比
考慮這樣一個應用,都有與配置相關的設定檔,這些設定檔都是由項目開發人員自訂的,在裡面定義一些需要的參數格式,當然在實際的項目中,這種設定檔,多採用xml格式,也有採用properties格式的,畢竟使用java的話,讀取properties檔案是比較方便的。
那麼要讀取properties檔案是比較方便的。
2.解決方案,代碼如下
package demo04.singleton.example1;import java.io.IOException;import java.io.InputStream;import java.util.Properties;/** * 讀取應用設定檔 */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();}}}}
package demo04.singleton.example1;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);}}
3.問題所在
上面的實現很簡單,但是我們自己仔細想想,這個有沒有問題呢?
看看這個用戶端使用的情況,只要是使用設定檔,都需要new一個 AppConfig對象,換句話說,在系統啟動並執行過程中,系統中會存在很多的AppConfig對象,如果設定檔的內容比較少的話,那麼對系統的運行效率和速度還沒有多大影響,但是如果設定檔的內容比較多的情況下,對系統資源的浪費就大大增加了,實際上,在系統運行,只要一個AppConfig對象就可以了。那麼這個問題應該如何解決呢?
4.使用單例模式(Singleton)來解決4.1單例模式定義:保證一個類僅有一個執行個體,並提供一個訪問它的全域訪問點。4.2單例模式範例程式碼如下:4.2.1懶漢式範例程式碼如下:
package demo04.singleton.example2;/** * 單例樣本 */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;}}
4.2.2惡漢式範例程式碼如下:
package demo04.singleton.example3;/** * 餓漢式單例實現的樣本 */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;}}
所謂惡漢式:就是建立對象執行個體的時候比較著急,所以裝載類的時候就建立對象執行個體。
private
static Singleton uniqueInstance =new Singleton();
所謂懶漢式:就是建立對象執行個體的時候不著急,只有在裝在對象的時候才會建立。
private
static Singleton uniqueInstance =null;
4.3使用單例模式重寫範例程式碼如下:
package demo04.singleton.example4;import java.io.IOException;import java.io.InputStream;import java.util.Properties;/** * 讀取應用設定檔,單例實現 */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 = AppConfig.class.getResourceAsStream("AppConfig.properties");try {p.load(in);// 把設定檔中的內容讀出來設定到屬性上this.parameterA = p.getProperty("paramA");this.parameterB = p.getProperty("paramB");} catch (IOException e) {System.out.println("裝載設定檔出錯了,具體堆棧資訊如下:");e.printStackTrace();}}}
package demo04.singleton.example4;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);}}
5.模式講解:5.1單例模式的功能:
單例模式是用來保證這個類在運行期間會被建立一個類執行個體,另外單例模式還提供了一個全域唯一一個訪問這個執行個體的訪問點,就是getInstance()方法。不管使用的是懶漢式還是惡漢式,這個全域訪問點是一樣的。
注意:這裡討論的單例模式並不適用於叢集環境,對於叢集環境下的單例這裡不去討論。
5.2單例模式的範圍:
目前java裡面實現的是 一個虛擬機器的範圍,因為裝載類的功能是虛擬機器的,所以一個虛擬機器通過在自己的ClassLoader中裝載類的時候,就會建立一個對象,那麼也就意味著,如果有很多ClassLoader的話,那麼就會有很多的對象。
5.3懶漢式與惡漢式的實現5.3.1懶漢式的實現:
第一個重點:它的私人化構造方法,那麼外部不能直接new出他的類。
第二個重點:它的getInstance()方法,這個就可以找到他的執行個體對象了。
5.3.2惡漢式的實現:
第一個重點:Static變數在類初始化的時候就會進行初始化
第二個重點:多個執行個體的static變數會共用一塊記憶體地區
5.3.3懶漢式與惡漢式的調用順序圖
5.4懶漢式的消極式載入的思想
消極式載入就是一開始不要載入資源,等到馬上要使用這個資源了或資料,然後在進行載入,這樣就會儘可能的節約資源。體現在如下地方:
public static synchronized Singleton getInstance() {// 判斷儲存執行個體的變數是否有值if (uniqueInstance == null) {// 如果沒有,就建立一個類執行個體,並把值賦值給儲存類執行個體的變數uniqueInstance = new Singleton();}// 如果有值,那就直接使用return uniqueInstance;}
5.5緩衝的思想
簡單的講就是,一個資源被頻繁的使用,而這些資源或資料存放區在系統外部,比如資料庫,硬碟檔案等。那麼每次操作這些資料的時候就要頻繁的從系統外部調用,那麼速度回很慢,造成效能問題。
一個簡單的解決辦法就是:把這些經常使用的資源緩衝在記憶體中,每次操作的時候,先在記憶體中找,如果有的話,就直接使用,如果沒有的話,就擷取它,那麼下次使用的時候就可以直接使用了。從而節約大量地時間。
緩衝是一種典型的空間換時間的方案。
範例程式碼如下
package demo04.singleton.example5;public class Singleton {// 4:定義一個變數來儲存建立好的類執行個體// 5:因為這個變數要在靜態方法中使用,所以需要加上static修飾private static Singleton instance = null;// 1:私人化構造方法,好在內部控制建立執行個體的數目private Singleton() {}// 2:定義一個方法來為用戶端提供類執行個體// 3:這個方法需要定義成類方法,也就是要加staticpublic static Singleton getInstance() {// 6:判斷儲存執行個體的變數是否有值if (instance == null) {// 6.1:如果沒有,就建立一個類執行個體,並把值賦值給儲存類執行個體的變數instance = new Singleton();}// 6.2:如果有值,那就直接使用return instance;}}
5.6java中緩衝的基本實現
先在緩衝裡面找,如果有需要使用的資料的話,那麼直接使用,如果沒有的話,那麼把這個資料設定到緩衝中,以備下次使用。
範例程式碼如下:
package demo04.singleton.example7;import java.util.HashMap;import java.util.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;}}
5.7利用緩衝來實現單例模式
範例程式碼如下:
package demo04.singleton.example8;import java.util.HashMap;import java.util.Map;/** * 使用緩衝來類比實現單例 */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;}}
5.8單例模式的優缺點:5.8.1時間和空間
懶漢式是典型的時間換空間
惡漢式是典型的空間換時間
5.8.2安全執行緒
不加同步的懶漢式是線程不安全的。會出現問題,所以需要加鎖。
5.8.3雙重檢查加鎖
即盡量減少不必要的同步塊,在樣本中體現在:只有instance==null的時候才要加鎖進行建立。
雙重檢查加鎖機制會使用一個volatile的關鍵字,它的意思是:被volatile修飾的值並不會被本地線程緩衝。由於這個關鍵字會屏蔽一些代碼最佳化,所以效率不是很高,所以不建議大量使用。
範例程式碼如下:
package demo04.singleton.example10;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;}}
5.9java中更好的一種單例模式
通過上面的討論發現,兩種單例模式都有小小的缺憾,那麼有沒有一種方法又能實現消極式載入,又能實現安全執行緒呢?
5.9.1基礎知識
類級內部類:由static修飾的成員式內部類,如果沒有static修飾的叫做對象式內部類。
類級內部類相當於其外部類的static 部分,與外部類對象之間不存在依賴關係。只有在第一次的時候才會被載入。
JVM隱式幫你執行同步的情況如下:
1.由靜態初始化器(在靜態欄位或static{}塊中的初始化器)初始化資料時
2.訪問final欄位時
3.在建立線程之前建立對象時
4.線程可以看見它將要處理的對象時。
5.9.2範例程式碼講解
package demo04.singleton.example11;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得到初始化,這個類在裝在並初始化的時候,會初始化它的靜態域,由於是靜態域,所以只會在虛擬機器裝載類的時候建立一次,並由虛擬機器來保證它的線程的安全性。
6.思考單例模式6.1單例模式的本質:控制執行個體數目
下面代碼示範一下
package demo04.singleton.example9;import java.util.HashMap;import java.util.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);}// 把當前執行個體的序號加1num++;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);}}
運行結果如下,看出1,4相同,2,5相同,3,6相同,這種方式也是線程不安全的。
t1==demo04.singleton.example9.OneExtend@153f67et2==demo04.singleton.example9.OneExtend@15bdc50t3==demo04.singleton.example9.OneExtend@1dd3812t4==demo04.singleton.example9.OneExtend@153f67et5==demo04.singleton.example9.OneExtend@15bdc50t6==demo04.singleton.example9.OneExtend@1dd3812
6.2何時選用單例模式?
當需要控制的類只能有一個的時候,而用戶端只能從一個全域訪問點訪問它時。