Java單例模式深刻詳解

原文地址:http://www.cnblogs.com/hxsyl/html

僅做爲筆記收藏……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的就是單例模式。設計模式

 1 class A(){
 2   private A(){}
 3   public name;
 4   
 5   pulbic static A creatInstance(){
 6     
 7        return new A();
 8   }
 9   
10 }
11 
12 A a = A.createInstance();
13 a.name; //name 屬性
 1 public class single{ 
 2 
 3     private static final single s=new single(); 
 4     
 5     private single(){ 
 6   
 7     } 
 8 
 9     public static single getInstance(){ 
10         return s; 
11 
12     } 
13 } 
複製代碼
 1 public class single{ 
 2 
 3     private static final single s=new single(); 
 4     
 5     private single(){ 
 6   
 7     } 
 8 
 9     public static single getInstance(){ 
10         return s; 
11 
12     } 
13 } 
複製代碼

二.單例模式概念及特色緩存

  java中單例模式是一種常見的設計模式,單例模式分三種:懶漢式單例、餓漢式單例、登記式單例三種。
  單例模式有一下特色:
  一、單例類只能有一個實例。
  二、單例類必須本身本身建立本身的惟一實例。
  三、單例類必須給全部其餘對象提供這一實例。安全

  單例模式確保某個類只有一個實例,並且自行實例化並向整個系統提供這個實例。在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具備資源管理器的功能。每臺計算機能夠有若干個打印機,但只能有一個Printer Spooler,以免兩個打印做業同時輸出到打印機中。每臺計算機能夠有若干通訊端口,系統應當集中管理這些通訊端口,以免一個通訊端口同時被兩個請求同時調用。總之,選擇單例模式就是爲了不不一致狀態,避免政出多頭。服務器

  正是因爲這個特 點,單例對象一般做爲程序中的存放配置信息的載體,由於它能保證其餘對象讀到一致的信息。例如在某個服務器程序中,該服務器的配置信息可能存放在數據庫或 文件中,這些配置數據由某個單例對象統一讀取,服務進程中的其餘對象若是要獲取這些配置信息,只需訪問該單例對象便可。這種方式極大地簡化了在複雜環境 下,尤爲是多線程環境下的配置管理,可是隨着應用場景的不一樣,也可能帶來一些同步問題。多線程

 

三.典型例題併發

  首先看一個經典的單例實現。

 1 public class Singleton {
 2  
 3     private static Singleton uniqueInstance = null;
 4  
 5  
 6  
 7     private Singleton() {
 8  
 9        // Exists only to defeat instantiation.
10  
11     }
12  
13  
14  
15     public static Singleton getInstance() {
16  
17        if (uniqueInstance == null) {
18  
19            uniqueInstance = new Singleton();
20  
21        }
22  
23        return uniqueInstance;
24  
25     }
26  
27     // Other methods...
28  
29 }

  Singleton經過將構造方法限定爲private避免了類在外部被實例化,在同一個虛擬機範圍內,Singleton的惟一實例只能經過getInstance()方法訪問。(事實上,經過Java反射機制是可以實例化構造方法爲private的類的,那基本上會使全部的Java單例實現失效。此問題在此處不作討論,姑且掩耳盜鈴地認爲反射機制不存在。)

  可是以上實現沒有考慮線程安全問題。所謂線程安全是指:若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。或者說:一個類或者程序所提供的接口對於線程來講是原子操做或者多個線程之間的切換不會致使該接口的執行結果存在二義性,也就是說咱們不用考慮同步的問題。顯然以上實現並不知足線程安全的要求,在併發環境下極可能出現多個Singleton實例。

 1 public class TestStream {
 2      private String name;
 3      public String getName() {
 4          return name;
 5      }
 6      public void setName(String name) {
 7          this.name = name;
 8      } 
 9      //該類只能有一個實例
10      private TestStream(){}    //私有無參構造方法
11      //該類必須自行建立
12      //有2種方式
13      /*private static final TestStream ts=new TestStream();*/
14      private static TestStream ts1=null;
15      //這個類必須自動向整個系統提供這個實例對象
16      public static TestStream getTest(){
17          if(ts1==null){
18              ts1=new TestStream();
19          }
20          return ts1;
21      }
22      public void getInfo(){
23          System.out.println("output message "+name);
24      }
25  }
 1 public class TestMain {
 2      public static void main(String [] args){
 3          TestStream s=TestStream.getTest();
 4          s.setName("張孝祥");
 5          System.out.println(s.getName());
 6          TestStream s1=TestStream.getTest();
 7          s1.setName("張孝祥");
 8          System.out.println(s1.getName());
 9          s.getInfo();
10          s1.getInfo();
11          if(s==s1){
12              System.out.println("建立的是同一個實例");
13          }else if(s!=s1){
14              System.out.println("建立的不是同一個實例");
15          }else{
16              System.out.println("application error");
17          }
18      }
19  }
複製代碼
 1 public class TestMain {
 2      public static void main(String [] args){
 3          TestStream s=TestStream.getTest();
 4          s.setName("張孝祥");
 5          System.out.println(s.getName());
 6          TestStream s1=TestStream.getTest();
 7          s1.setName("張孝祥");
 8          System.out.println(s1.getName());
 9          s.getInfo();
10          s1.getInfo();
11          if(s==s1){
12              System.out.println("建立的是同一個實例");
13          }else if(s!=s1){
14              System.out.println("建立的不是同一個實例");
15          }else{
16              System.out.println("application error");
17          }
18      }
19  }
複製代碼

運行結果:
  張孝祥
  張孝祥
  output message 張孝祥
  output message 張孝祥
  建立的是同一個實例
 
結論:由結果能夠得知單例模式爲一個面向對象的應用程序提供了對象唯一的訪問點,無論它實現何種功能,整個應用程序都會同享一個實例對象。

  其次,下面是單例的三種實現。    

    1.餓漢式單例類

  飛哥下面這個能夠不加final,由於靜態方法只在編譯期間執行一次初始化,也就是隻會有一個對象。

 1 //餓漢式單例類.在類初始化時,已經自行實例化 
 2  public class Singleton1 {
 3      //私有的默認構造子
 4      private Singleton1() {}
 5      //已經自行實例化 
 6      private static final Singleton1 single = new Singleton1();
 7      //靜態工廠方法 
 8      public static Singleton1 getInstance() {
 9          return single;
10      }
11  }

    2.懶漢式單例類

  那個if判斷確保對象只建立一次。

 1 //懶漢式單例類.在第一次調用的時候實例化 
 2  public class Singleton2 {
 3      //私有的默認構造子
 4      private Singleton2() {}
 5      //注意,這裏沒有final    
 6      private static Singleton2 single=null;
 7      //靜態工廠方法 
 8      public synchronized  static Singleton2 getInstance() {
 9           if (single == null) {  
10               single = new Singleton2();
11           }  
12          return single;
13      }
14  }

     3.登記式單例類

 1 import java.util.HashMap;
 2  import java.util.Map;
 3  //登記式單例類.
 4  //相似Spring裏面的方法,將類名註冊,下次從裏面直接獲取。
 5  public class Singleton3 {
 6      private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
 7      static{
 8          Singleton3 single = new Singleton3();
 9          map.put(single.getClass().getName(), single);
10      }
11      //保護的默認構造子
12      protected Singleton3(){}
13      //靜態工廠方法,返還此類唯一的實例
14      public static Singleton3 getInstance(String name) {
15          if(name == null) {
16              name = Singleton3.class.getName();
17              System.out.println("name == null"+"--->name="+name);
18          }
19          if(map.get(name) == null) {
20              try {
21                  map.put(name, (Singleton3) Class.forName(name).newInstance());
22              } catch (InstantiationException e) {
23                  e.printStackTrace();
24              } catch (IllegalAccessException e) {
25                  e.printStackTrace();
26              } catch (ClassNotFoundException e) {
27                  e.printStackTrace();
28              }
29          }
30          return map.get(name);
31      }
32      //一個示意性的商業方法
33      public String about() {    
34          return "Hello, I am RegSingleton.";    
35      }    
36      public static void main(String[] args) {
37          Singleton3 single3 = Singleton3.getInstance(null);
38          System.out.println(single3.about());
39      }
40  }

 

四.單例對象做配置信息管理時可能會帶來的幾個同步問題
  

  1.在多線程環境下,單例對象的同步問題主要體如今兩個方面,單例對象的初始化和單例對象的屬性更新。

    本文描述的方法有以下假設:

    a. 單例對象的屬性(或成員變量)的獲取,是經過單例對象的初始化實現的。也就是說,在單例對象初始化時,會從文件或數據庫中讀取最新的配置信息。

    b. 其餘對象不能直接改變單例對象的屬性,單例對象屬性的變化來源於配置文件或配置數據庫數據的變化。

    1.1單例對象的初始化

      首先,討論一下單例對象的初始化同步。單例模式的一般處理方式是,在對象中有一個靜態成員變量,其類型就是單例類型自己;若是該變量爲null,則建立該單例類型的對象,並將該變量指向這個對象;若是該變量不爲null,則直接使用該變量。   

      這種處理方式在單線程的模式下能夠很好的運行;可是在多線程模式下,可能產生問題。若是第一個線程發現成員變量爲null,準備建立對象;這是第二 個線程同時也發現成員變量爲null,也會建立新對象。這就會形成在一個JVM中有多個單例類型的實例。若是這個單例類型的成員變量在運行過程當中變化,會 形成多個單例類型實例的不一致,產生一些很奇怪的現象。例如,某服務進程經過檢查單例對象的某個屬性來中止多個線程服務,若是存在多個單例對象的實例,就 會形成部分線程服務中止,部分線程服務不能中止的狀況。

    1.2單例對象的屬性更新

      一般,爲了實現配置信息的實時更新,會有一個線程不停檢測配置文件或配置數據庫的內容,一旦發現變化,就更新到單例對象的屬性中。在更新這些信 息的時候,極可能還會有其餘線程正在讀取這些信息,形成意想不到的後果。仍是以經過單例對象屬性中止線程服務爲例,若是更新屬性時讀寫不一樣步,可能訪問該 屬性時這個屬性正好爲空(null),程序就會拋出異常。

      下面是解決方法

 1 //單例對象的初始化同步
 2 public class GlobalConfig {
 3     private static GlobalConfig instance = null;
 4     private Vector properties = null;
 5     private GlobalConfig() {
 6       //Load configuration information from DB or file
 7       //Set values for properties
 8     }
 9     private static synchronized void syncInit() {
10       if (instance == null) {
11         instance = new GlobalConfig();
12       }
13     }
14     public static GlobalConfig getInstance() {
15       if (instance == null) {
16         syncInit();
17       }
18       return instance;
19     }
20     public Vector getProperties() {
21       return properties;
22     }
23   }

  這種處理方式雖然引入了同步代碼,可是由於這段同步代碼只會在最開始的時候執行一次或屢次,因此對整個系統的性能不會有影響。

  單例對象的屬性更新同步。

  參照讀者/寫者的處理方式,設置一個讀計數器,每次讀取配置信息前,將計數器加1,讀完後將計數器減1.只有在讀計數器爲0時,才能更新數據,同時要阻塞全部讀屬性的調用。

  代碼以下:

 1 public class GlobalConfig {
 2  private static GlobalConfig instance;
 3  private Vector properties = null;
 4  private boolean isUpdating = false;
 5  private int readCount = 0;
 6  private GlobalConfig() {
 7    //Load configuration information from DB or file
 8       //Set values for properties
 9  }
10  private static synchronized void syncInit() {
11   if (instance == null) {
12    instance = new GlobalConfig();
13   }
14  }
15  public static GlobalConfig getInstance() {
16   if (instance==null) {
17    syncInit();
18   }
19   return instance;
20  }
21  public synchronized void update(String p_data) {
22   syncUpdateIn();
23   //Update properties
24  }
25  private synchronized void syncUpdateIn() {
26   while (readCount > 0) {
27    try {
28     wait();
29    } catch (Exception e) {
30    }
31   }
32  }
33  private synchronized void syncReadIn() {
34   readCount++;
35  }
36  private synchronized void syncReadOut() {
37   readCount--;
38   notifyAll();
39  }
40  public Vector getProperties() {
41   syncReadIn();
42   //Process data
43   syncReadOut();
44   return properties;
45  }
46   }

 

  採用"影子實例"的辦法具體說,就是在更新屬性時,直接生成另外一個單例對象實例,這個新生成的單例對象實例將從數據庫或文件中讀取最新的配置信息;而後將這些配置信息直接賦值給舊單例對象的屬性。

 1 public class GlobalConfig {
 2     private static GlobalConfig instance = null;
 3     private Vector properties = null;
 4     private GlobalConfig() {
 5       //Load configuration information from DB or file
 6       //Set values for properties
 7     }
 8     private static synchronized void syncInit() {
 9       if (instance = null) {
10         instance = new GlobalConfig();
11       }
12     }
13     public static GlobalConfig getInstance() {
14       if (instance = null) {
15         syncInit();
16       }
17       return instance;
18     }
19     public Vector getProperties() {
20       return properties;
21     }
22     public void updateProperties() {
23       //Load updated configuration information by new a GlobalConfig object
24       GlobalConfig shadow = new GlobalConfig();
25       properties = shadow.getProperties();
26     }
27   }

注意:在更新方法中,經過生成新的GlobalConfig的實例,從文件或數據庫中獲得最新配置信息,並存放到properties屬性中。上面兩個方法比較起來,第二個方法更好,首先,編程更簡單;其次,沒有那麼多的同步操做,對性能的影響也不大。

相關文章
相關標籤/搜索