Java單例模式

1. 實現(xiàn)單例模式

  1. 餓漢模式和懶漢模式
    單例模式根據(jù)實例化時機分為餓漢模式和懶漢模式。
    餓漢模式,是指不等到單例真正使用時在去創(chuàng)建,而是在類加載或者系統(tǒng)初始化就創(chuàng)建好。
    懶漢模式中單例要等到第一次使用時才創(chuàng)建。

  2. 餓漢模式
    最簡單的實現(xiàn)

    class Singleton{
        private static Singleton instance = new Singleton();
        private Singleton(){};
        public static Singleton getInstance(){return instance;}
    }
    

    上面是一種線程安全的實現(xiàn)方式,因為instance是類靜態(tài)成員,會在類加載并初始化時創(chuàng)建,因此可以保證即便是不同線程也會獲得同一份實例(這句話在有些情況下并不正確,比如通過序列化,反射的方式還是能夠創(chuàng)建多個實例出來)。

  3. 懶漢模式

    相對于1中在加載的時候就創(chuàng)建,另一種則是在首次使用時創(chuàng)建,比如下面這種方式:

    class Singleton{
         priavte static Singleton instance = null;
         private Singleton(){};
         public static Singleton getInstance(){
             if(null == instance){
                 instance = new Singleton();
             }
             
             return instance;
         }
     }
    

    上面的這種形式,在首次調(diào)用getInstance時才會創(chuàng)建單例,但是它有一個問題就是,在多線程的情況下有可能會創(chuàng)建出多個實例化對象出來:比如線程1和線程2同時判斷null == instance為true,結(jié)果進入下一步兩個線程就創(chuàng)建兩個instance出來。當然這種方式通過加鎖或則使用synchronize關(guān)鍵字的方式就可以避免了。這里不展示對整個getInstance方法加鎖的實現(xiàn),而是展示另一種方式:

    3.1 兩次判斷,代碼如下:

    class Singleton{
         priavte static volatile Singleton instance = null;
         private Singleton(){};
         public static Singleton getInstance(){
             if(null == instance){
              synchronize(Singleton.class){
                 if(null == instance){
                     instance = new Singleton();
                 }
              }
             }
             return instance;
         }
     }
    

    比起對整個getInstance方法加鎖,兩次判斷的方式可以避免一些不必要的加鎖開銷。

    同時volatile關(guān)鍵字十分必要,多核環(huán)境下,多線程分布在多個核上,每個核心擁有各自的cache,讀取數(shù)據(jù)總會嘗試從cache讀取。那就意味著instance = new Singleton();可能不會立即被運行在其他核心上的線程所知,導致即便instance更新后,其他線程cache中instance依然是null。volatile關(guān)鍵字保存每次更新都會更新到內(nèi)存,同時保存其他核心上該緩存項失效,需要從內(nèi)存讀取。

    3.2 內(nèi)部類實現(xiàn)延遲加載
    上面兩次判斷的方法依然是通過加鎖的方式來保證多線程情況下的創(chuàng)建單一實例,回顧1的實現(xiàn)中,保證只有一個實例是通過jvm只初始化一次static類成員這一機制實現(xiàn)的,但是1中在Singleton類加載的時候就會實例化靜態(tài)成員instance,這可不是我們想要的首次使用創(chuàng)建這一目的。為了達到這一目的,我們可以借助內(nèi)部類的方式實現(xiàn),下面是代碼實現(xiàn):

    class Singleton{
         private Singleton(){};
         
         private static class SingletonHolder{
             priavte static Singleton instance = new Singleton(); 
         }
         
         public static Singleton getInstance(){return SingletonHolder.instance;}
     }
    

    jvm加載Singleton時并不會加載其SingletonHolder,因此instance就不會被早早的創(chuàng)建,直到調(diào)用getInstance方法時才回加載SingletonHolder,而instance是其靜態(tài)成員,jvm保證了它只此一份。

附:關(guān)于類的加載時機
「深入理解java虛擬機」一書中有介紹過類什么時候被初始化:

  1. 創(chuàng)建類的實例時
  2. 使用Class.forName時
  3. 訪問類的靜態(tài)成員
  4. 調(diào)用類的靜態(tài)方法
  5. 子類初始化時,父類也會初始化

2.實現(xiàn)單例模式的問題

在java中創(chuàng)建一個對象,我們可以通過:new,clone,序列化,反射。上面單例模式的實現(xiàn)我們通過將構(gòu)造函數(shù)私有化使得不能通過new來創(chuàng)建對象,但是其他的手段依然可以,下面舉例說明:

  1. 反射
    通過反射我們可以訪問類的私有構(gòu)造函授,測試代碼如下(單例代碼見上面1):

    public class TestSingleton {
     public static void main(String args[]){
         try {
             Constructor cons = Singleton.class.getDeclaredConstructor();
             cons.setAccessible(true);
             Singleton instance1 = Singleton.getInstance();
             Singleton instance2 = (Singleton)cons.newInstance();
    
             System.out.println("instance1 == instance2 ?"+(instance1 == instance2));
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
     } 
    

    打印的結(jié)果如下:
    instance1 == instance2 ? false
    instance1和instance2是不同對象,因此這就破壞了單例模式,網(wǎng)上提供解決反射帶來的問題也十分簡單,只需要修改構(gòu)造函數(shù),使得它第二次以及更多次的調(diào)用拋出異常,修改構(gòu)造函數(shù)如下:

    private static boolean flag = false;
    public Singleton(){
         if(false == flag){
             flag = true;
         }else{
              throw new Exception(...);
         }
    }
    

    不過java的反射有點沒節(jié)操,你還是可以修改flag值,我的天。
    在《effective java》里提供一種解決之道,可以無視反射,那就是通過枚舉來實現(xiàn)。像下面這樣:

    public enum Singleton3 {
     INSTANCE;
    
     public void applaud(){
         System.out.println("haha, go home,reflection!");
     }
     }
    

    沒有構(gòu)造函數(shù)了。。。(跟jvm初始化枚舉變量的方式有關(guān)系,當你再試圖通過反射獲取構(gòu)造函數(shù)會拋出異常),所以再嘗試通過反射獲得構(gòu)造函數(shù),就會拋異常。

  2. 序列化的影響
    不考慮枚舉實現(xiàn)單例模式,如果Singleton實現(xiàn)了Serializable接口,那么如果我們將Singleton序列到一個對象中去,在反序列化出來,就會導致不同的實例,請看下面代碼:

    public class TestSingleton2 {
    
     public static void main(String []args){
         try {
             Singleton instance = Singleton.getInstance();
    
             //將instance序列化到文件singleton中.
             FileOutputStream fos = new FileOutputStream("singleton");
             ObjectOutputStream oos = new ObjectOutputStream(fos);
    
             oos.writeObject(instance);
    
             //從文件singleton中讀出對象
             FileInputStream fis = new FileInputStream("singleton");
             ObjectInputStream ois = new ObjectInputStream(fis);
    
             Singleton instance1 = (Singleton)ois.readObject();
    
             System.out.println("instance == instance1 ? " + (instance == instance1));
    
         } catch (Exception e) {
             e.printStackTrace();
         }
    
     }
     }
    

結(jié)果顯示instance和instance1為兩個實例。

序列化前后產(chǎn)生不同對象,解決方法也很簡單,jvm在反序列化時,如果該類實現(xiàn)的下面方法:
private Object readResolve() throw IOException
那么就會調(diào)用這個方法返回對象,以替換流中對象。因此可以在這個方法里返回Singleton的instance成員,如下:

 private  Object readResolve() throws ObjectStreamException{
     return instance;
   }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 本文主要參考 那些年,我們一起寫過的“單例模式”。 何為單例模式? 顧名思義,單例模式就是保證一個類僅有一個...
    tandeneck閱讀 2,623評論 1 8
  • java 單例模式指整個程序中只有一個某個類的實例,通常被用來代表那些本質(zhì)上唯一的系統(tǒng)組件,比如窗口管理器或者文件...
    dcme閱讀 1,125評論 0 10
  • 1 場景問題# 1.1 讀取配置文件的內(nèi)容## 考慮這樣一個應用,讀取配置文件的內(nèi)容。 很多應用項目,都有與應用相...
    七寸知架構(gòu)閱讀 6,965評論 12 68
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,623評論 18 399
  • 學習了地圖及大頭針的使用。發(fā)現(xiàn)還是蠻 簡單的就能實現(xiàn)對地圖的操作。 首先,我們要了解蘋果的定位組件: Wifi定位...
    曉龍歌閱讀 443評論 0 0

友情鏈接更多精彩內(nèi)容