Java單例模式

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

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

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

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

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

  3. 懶漢模式

    相對(duì)于1中在加載的時(shí)候就創(chuàng)建,另一種則是在首次使用時(shí)創(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時(shí)才會(huì)創(chuàng)建單例,但是它有一個(gè)問題就是,在多線程的情況下有可能會(huì)創(chuàng)建出多個(gè)實(shí)例化對(duì)象出來:比如線程1和線程2同時(shí)判斷null == instance為true,結(jié)果進(jìn)入下一步兩個(gè)線程就創(chuàng)建兩個(gè)instance出來。當(dāng)然這種方式通過加鎖或則使用synchronize關(guān)鍵字的方式就可以避免了。這里不展示對(duì)整個(gè)getInstance方法加鎖的實(shí)現(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;
         }
     }
    

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

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

    3.2 內(nèi)部類實(shí)現(xiàn)延遲加載
    上面兩次判斷的方法依然是通過加鎖的方式來保證多線程情況下的創(chuàng)建單一實(shí)例,回顧1的實(shí)現(xiàn)中,保證只有一個(gè)實(shí)例是通過jvm只初始化一次static類成員這一機(jī)制實(shí)現(xiàn)的,但是1中在Singleton類加載的時(shí)候就會(huì)實(shí)例化靜態(tài)成員instance,這可不是我們想要的首次使用創(chuàng)建這一目的。為了達(dá)到這一目的,我們可以借助內(nèi)部類的方式實(shí)現(xiàn),下面是代碼實(shí)現(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時(shí)并不會(huì)加載其SingletonHolder,因此instance就不會(huì)被早早的創(chuàng)建,直到調(diào)用getInstance方法時(shí)才回加載SingletonHolder,而instance是其靜態(tài)成員,jvm保證了它只此一份。

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

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

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

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

  1. 反射
    通過反射我們可以訪問類的私有構(gòu)造函授,測(cè)試代碼如下(單例代碼見上面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是不同對(duì)象,因此這就破壞了單例模式,網(wǎng)上提供解決反射帶來的問題也十分簡(jiǎn)單,只需要修改構(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的反射有點(diǎn)沒節(jié)操,你還是可以修改flag值,我的天。
    在《effective java》里提供一種解決之道,可以無視反射,那就是通過枚舉來實(shí)現(xiàn)。像下面這樣:

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

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

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

    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中讀出對(duì)象
             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為兩個(gè)實(shí)例。

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

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

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

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

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