Java單例模式深入詳解
Java單例模式深入詳解
一.問題引入
偶然想想到的如果把Java的構造方法弄成private,那裏面的成員屬性是不是只有通過static來訪問呢;如果構造方法是private的話,那麽有什麽好處呢;如果構造方法是private的話,會不更好的封裝該內呢?我主要是應用在使用普通類模擬枚舉類型裏,後來發現這就是傳說中的單例模式。構造函數弄成private 就是單例模式,即不想讓別人用new 方法來創建多個對象,可以在類裏面先生成一個對象,然後寫一個public static方法把這個對象return出去。(eg:public 類名 getInstancd(){return 你剛剛生成的那個類對象;}),用static是因為你的構造函數是私有的,不能產生對象,所以只能用類名調用,所有只能是靜態函數。成員變量也可以寫getter/setter供外界訪問的。如果誰要用這個類的實例就用有興趣的讀者參看我的這一篇博文http://www.cnblogs.com/hxsyl/archive/2013/03/18/2966360.html。
第一個代碼不是單例模式,也就是說不一定只要構造方法是private的就是單例模式。
class A(){
private A(){}
public name;
pulbic static A creatInstance(){
return new A();
}
}
A a = A.createInstance();
a.name; //name 屬性
public class single{
private static final single s=new single();
private single(){
}
public static single getInstance(){
return s;
}
}
二.單例模式概念及特點
java中單例模式是一種常見的設計模式,單例模式分三種:懶漢式單例、餓漢式單例、登記式單例三種。
單例模式有一下特點:
1、單例類只能有一個實例。
2、單例類必須自己自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若幹個打印機,但只能有一個Printer Spooler,以避免兩個打印作業同時輸出到打印機中。每臺計算機可以有若幹通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。
正是由於這個特 點,單例對象通常作為程序中的存放配置信息的載體,因為它能保證其他對象讀到一致的信息。例如在某個服務器程序中,該服務器的配置信息可能存放在數據庫或 文件中,這些配置數據由某個單例對象統一讀取,服務進程中的其他對象如果要獲取這些配置信息,只需訪問該單例對象即可。這種方式極大地簡化了在復雜環境 下,尤其是多線程環境下的配置管理,但是隨著應用場景的不同,也可能帶來一些同步問題。
三.典型例題
首先看一個經典的單例實現。
public class Singleton {
private static Singleton uniqueInstance = null;
private Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
} // Other methods...
}
Singleton通過將構造方法限定為private避免了類在外部被實例化,在同一個虛擬機範圍內,Singleton的唯一實例只能通過getInstance()方法訪問。(事實上,通過Java反射機制是能夠實例化構造方法為private的類的,那基本上會使所有的Java單例實現失效。此問題在此處不做討論,姑且掩耳盜鈴地認為反射機制不存在。)
但是以上實現沒有考慮線程安全問題。所謂線程安全是指:如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。或者說:一個類或者程序所提供的接口對於線程來說是原子操作或者多個線程之間的切換不會導致該接口的執行結果存在二義性,也就是說我們不用考慮同步的問題。顯然以上實現並不滿足線程安全的要求,在並發環境下很可能出現多個Singleton實例。
public class TestStream {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//該類只能有一個實例
private TestStream(){} //私有無參構造方法
//該類必須自行創建
//有2種方式
/*private static final TestStream ts=new TestStream();*/
private static TestStream ts1=null;
//這個類必須自動向整個系統提供這個實例對象
public static TestStream getTest(){
if(ts1==null){
ts1=new TestStream();
}
return ts1;
}
public void getInfo(){
System.out.println("output message "+name);
}
}
public class TestMain {
public static void main(String [] args){
TestStream s=TestStream.getTest();
s.setName("張孝祥");
System.out.println(s.getName());
TestStream s1=TestStream.getTest();
s1.setName("張孝祥");
System.out.println(s1.getName());
s.getInfo();
s1.getInfo();
if(s==s1){
System.out.println("創建的是同一個實例");
}else if(s!=s1){
System.out.println("創建的不是同一個實例");
}else{
System.out.println("application error");
}
}
}
運行結果:
張孝祥
張孝祥
output message 張孝祥
output message 張孝祥
創建的是同一個實例
結論:由結果可以得知單例模式為一個面向對象的應用程序提供了對象惟一的訪問點,不管它實現何種功能,整個應用程序都會同享一個實例對象。
其次,下面是單例的三種實現。
1.餓漢式單例類
飛哥下面這個可以不加final,因為靜態方法只在編譯期間執行一次初始化,也就是只會有一個對象。
//餓漢式單例類.在類初始化時,已經自行實例化
public class Singleton1 {
//私有的默認構造子
private Singleton1() {}
//已經自行實例化
private static final Singleton1 single = new Singleton1();
//靜態工廠方法
public static Singleton1 getInstance() {
return single;
}
}
2.懶漢式單例類
那個if判斷確保對象只創建一次。
//懶漢式單例類.在第一次調用的時候實例化
public class Singleton2 {
//私有的默認構造子
private Singleton2() {}
//註意,這裏沒有final
private static Singleton2 single=null;
//靜態工廠方法
public synchronized static Singleton2 getInstance() {
if (single == null) {
single = new Singleton2();
}
return single;
}
}
3.登記式單例類
import java.util.HashMap;
import java.util.Map;
//登記式單例類.
//類似Spring裏面的方法,將類名註冊,下次從裏面直接獲取。
public class Singleton3 {
private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
static{
Singleton3 single = new Singleton3();
map.put(single.getClass().getName(), single);
}
//保護的默認構造子
protected Singleton3(){}
//靜態工廠方法,返還此類惟一的實例
public static Singleton3 getInstance(String name) {
if(name == null) {
name = Singleton3.class.getName();
System.out.println("name == null"+"--->name="+name);
}
if(map.get(name) == null) {
try {
map.put(name, (Singleton3) Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return map.get(name);
}
//一個示意性的商業方法
public String about() {
return "Hello, I am RegSingleton.";
}
public static void main(String[] args) {
Singleton3 single3 = Singleton3.getInstance(null);38 System.out.println(single3.about());
}
}
四.單例對象作配置信息管理時可能會帶來的幾個同步問題
1.在多線程環境下,單例對象的同步問題主要體現在兩個方面,單例對象的初始化和單例對象的屬性更新。
本文描述的方法有如下假設:
a. 單例對象的屬性(或成員變量)的獲取,是通過單例對象的初始化實現的。也就是說,在單例對象初始化時,會從文件或數據庫中讀取最新的配置信息。
b. 其他對象不能直接改變單例對象的屬性,單例對象屬性的變化來源於配置文件或配置數據庫數據的變化。
1.1單例對象的初始化
首先,討論一下單例對象的初始化同步。單例模式的通常處理方式是,在對象中有一個靜態成員變量,其類型就是單例類型本身;如果該變量為null,則創建該單例類型的對象,並將該變量指向這個對象;如果該變量不為null,則直接使用該變量。
這種處理方式在單線程的模式下可以很好的運行;但是在多線程模式下,可能產生問題。如果第一個線程發現成員變量為null,準備創建對象;這是第二 個線程同時也發現成員變量為null,也會創建新對象。這就會造成在一個JVM中有多個單例類型的實例。如果這個單例類型的成員變量在運行過程中變化,會 造成多個單例類型實例的不一致,產生一些很奇怪的現象。例如,某服務進程通過檢查單例對象的某個屬性來停止多個線程服務,如果存在多個單例對象的實例,就 會造成部分線程服務停止,部分線程服務不能停止的情況。
1.2單例對象的屬性更新
通常,為了實現配置信息的實時更新,會有一個線程不停檢測配置文件或配置數據庫的內容,一旦發現變化,就更新到單例對象的屬性中。在更新這些信 息的時候,很可能還會有其他線程正在讀取這些信息,造成意想不到的後果。還是以通過單例對象屬性停止線程服務為例,如果更新屬性時讀寫不同步,可能訪問該 屬性時這個屬性正好為空(null),程序就會拋出異常。
下面是解決方法
//單例對象的初始化同步
public class GlobalConfig {
private static GlobalConfig instance = null;
private Vector properties = null;
private GlobalConfig() {
//Load configuration information from DB or file
//Set values for properties
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new GlobalConfig();
}
}
public static GlobalConfig getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
public Vector getProperties() {
return properties;
}
}
這種處理方式雖然引入了同步代碼,但是因為這段同步代碼只會在最開始的時候執行一次或多次,所以對整個系統的性能不會有影響。
單例對象的屬性更新同步。
參照讀者/寫者的處理方式,設置一個讀計數器,每次讀取配置信息前,將計數器加1,讀完後將計數器減1.只有在讀計數器為0時,才能更新數據,同時要阻塞所有讀屬性的調用。
代碼如下:
public class GlobalConfig {
private static GlobalConfig instance;
private Vector properties = null;
private boolean isUpdating = false;
private int readCount = 0;
private GlobalConfig() {
//Load configuration information from DB or file
//Set values for properties
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new GlobalConfig();
}
}
public static GlobalConfig getInstance() {
if (instance==null) {
syncInit();
}
return instance;
}
public synchronized void update(String p_data) {
syncUpdateIn();
//Update properties24 }
private synchronized void syncUpdateIn() {
while (readCount > 0) {
try {
wait();
} catch (Exception e) {
}
}
}
private synchronized void syncReadIn() {
readCount++;
}
private synchronized void syncReadOut() {
readCount--;
notifyAll();
}
public Vector getProperties() {
syncReadIn();
//Process data43 syncReadOut();
return properties;
}
}
采用"影子實例"的辦法具體說,就是在更新屬性時,直接生成另一個單例對象實例,這個新生成的單例對象實例將從數據庫或文件中讀取最新的配置信息;然後將這些配置信息直接賦值給舊單例對象的屬性。
public class GlobalConfig {
private static GlobalConfig instance = null;
private Vector properties = null;
private GlobalConfig() {
//Load configuration information from DB or file
//Set values for properties
}
private static synchronized void syncInit() {
if (instance = null) {
instance = new GlobalConfig();
}
}
public static GlobalConfig getInstance() {
if (instance = null) {
syncInit();
}
return instance;
}
public Vector getProperties() {
return properties;
}
public void updateProperties() {
//Load updated configuration information by new a GlobalConfig object24 GlobalConfig shadow = new GlobalConfig();
properties = shadow.getProperties();
}
}
註意:在更新方法中,通過生成新的GlobalConfig的實例,從文件或數據庫中得到最新配置信息,並存放到properties屬性中。上面兩個方法比較起來,第二個方法更好,首先,編程更簡單;其次,沒有那麽多的同步操作,對性能的影響也不大。
Java單例模式深入詳解