① 設計模式的藝術-01.單例(Singleton)模式

單例模式爲什麼要出現

  在工做過程當中,發現全部可使用單例模式的類都有一個共性,那就是這個類沒有本身的狀態,換句話說,這些類不管你實例化多少個,其實都是同樣的。spring

  若是咱們不將這個類控制成單例的結構,應用中就會存在不少如出一轍的類實例,這會很是浪費系統的內存資源,並且容易致使錯誤甚至必定會產生錯誤,數據庫

因此咱們單例模式所期待的目標或者說使用它的目的,是爲了儘量的節約內存空間,減小無謂的GC消耗,而且使應用能夠正常運做。編程

常見應用場景

  Windows的Task Manager(任務管理器)就是很典型的單例模式windows

  windows的Recycle Bin(回收站)也是典型的單例應用。在整個系統運行過程當中,回收站一直維護着僅有的一個實例。安全

  項目中,讀取配置文件的類,通常也只有一個對象。沒有必要每次使用配置文件數據,每次new一個對象去讀取。多線程

  網站的計數器,通常也是採用單例模式實現,不然難以同步。併發

  應用程序的日誌應用,通常都何用單例模式實現,這通常是因爲共享的日誌文件一直處於打開狀態,由於只能有一個實例去操做,不然內容很差追加。框架

  數據庫鏈接池的設計通常也是採用單例模式,由於數據庫鏈接是一種數據庫資源。ide

  操做系統的文件系統,也是大的單例模式實現的具體例子,一個操做系統只能有一個文件系統。性能

  Application 也是單例的典型應用(Servlet編程中會涉及到)

  在Spring中,每一個Bean默認就是單例的,這樣作的優勢是Spring容器能夠管理

  在servlet編程中,每一個Servlet也是單例

  在spring MVC框架/struts1框架中,控制器對象也是單例

單例模式的優勢

  因爲單例模式只生成一個實例,減小了系統性能開銷,當一個對象的產生須要比較多的資源時,如讀取配置、產生其餘依賴對象時,則能夠經過在應用啓動時直接產生一個單例對象,而後永久駐留內存的方式來解決單例模式能夠在系統設置全局的訪問點,優化環共享資源訪問,例如能夠設計一個單例類,負責全部數據表的映射處理。

1、餓漢式實現(單例對象當即加載)

public class SingletonDemo1 {
    
    //類初始化時,當即加載這個對象(沒有延時加載的優點)。加載類時,自然的是線程安全的!
    private static SingletonDemo1 instance = new SingletonDemo1();  
    
    private SingletonDemo1(){
    }
    
    //方法沒有同步,調用效率高!
    public static SingletonDemo1  getInstance(){
        return instance;
    }
    
}

餓漢式單例模式代碼中,static變量會在類裝載時初始化,此時也不會涉及多個線程對象訪問該對象的問題。虛擬機保證只會裝載一次該類,確定不會發生併發訪問的問題。

所以,能夠省略synchronized關鍵字。

問題:若是隻是加載本類,而不是要調用getInstance(),甚至永遠沒有調用,則會形成資源浪費!

2、懶漢式實現(單例對象延遲加載)

public class SingletonDemo2 {
    
    //類初始化時,不初始化這個對象(延時加載,真正用的時候再建立)。
    private static SingletonDemo2 instance;  
    
    private SingletonDemo2(){ //私有化構造器
    }
    
    //方法同步,調用效率低!
    public static  synchronized SingletonDemo2  getInstance(){
        if(instance==null){
            instance = new SingletonDemo2();
        }
        return instance;
    }
    
}

要點:
    lazy load! 延遲加載, 懶加載! 真正用的時候才加載!
問題:
    資源利用率高了。可是,每次調用getInstance()方法都要同步,併發效率較低。

 3、雙重檢查鎖實現單例模式

public class SingletonDemo3 { 

  private static SingletonDemo3 instance = null; 

  public static SingletonDemo3 getInstance() { 
    if (instance == null) { 
      SingletonDemo3 sc; 
      synchronized (SingletonDemo3.class) { 
        sc = instance; 
        if (sc == null) { 
          synchronized (SingletonDemo3.class) { 
            if(sc == null) { 
              sc = new SingletonDemo3(); 
            } 
          } 
          instance = sc; 
        } 
      } 
    } 
    return instance; 
  } 

  private SingletonDemo3() { 

  } 
    
}

這個模式將同步內容下方到if內部,提升了執行的效率 沒必要每次獲取對象時都進行同步,只有第一次才同步 建立了之後就不必了。

問題:

因爲編譯器優化緣由和JVM底層內部模型緣由,

偶爾會出問題。不建議使用。

4、靜態內部類實現方式(也是一種懶加載方式)

public class SingletonDemo4 {
    
    private static class SingletonClassInstance {
        private static final SingletonDemo4 instance = new SingletonDemo4();
    }
    
    private SingletonDemo4(){
    }
    
    //方法沒有同步,調用效率高!
    public static SingletonDemo4  getInstance(){
        return SingletonClassInstance.instance;
    }
    
}

要點:

外部類沒有static屬性,則不會像餓漢式那樣當即加載對象。

只有真正調用getInstance(),纔會加載靜態內部類。加載類時是線程 安全的。 instance是static final類型,保證了內存中只有這樣一個實例存在,並且只能被賦值一次,從而保證了線程安全性.

兼備了併發高效調用和延遲加載的優點!

5、使用枚舉實現單例模式

public enum SingletonDemo5 {
    
    //這個枚舉元素,自己就是單例對象!
    INSTANCE;
    
    //添加本身須要的操做!
    public void singletonOperation(){
    }
    
}

優勢: 實現簡單 枚舉自己就是單例模式。由JVM從根本上提供保障!避免經過反射和反序列化的漏洞!

缺點: 無延遲加載

常見的五種單例模式在多線程環境下的效率測試

CountDownLatch

同步輔助類,在完成一組正在其餘線程中執行的操做以前,它容許一個或多個線程一直等待。

countDown() 當前線程調此方法,則計數減一(建議放在 finally裏執行)

await(), 調用此方法會一直阻塞當前線程,直到計時器的值爲0

public static void main(String[] args) throws Exception {
        
        long start = System.currentTimeMillis();
        int threadNum = 10;
        final CountDownLatch  countDownLatch = new CountDownLatch(threadNum);
        
        for(int i=0;i<threadNum;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0;i<1000000;i++){
                        //Object o = SingletonDemo4.getInstance();
                        Object o = SingletonDemo5.INSTANCE;
                    }
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();    //main線程阻塞,直到計數器變爲0,纔會繼續往下執行!
        long end = System.currentTimeMillis();
        System.out.println("總耗時:"+(end-start));
    }

總結

主要:

餓漢式(線程安全,調用效率高。 可是,不能延時加載。)

懶漢式(線程安全,調用效率不高。 可是,能夠延時加載。)

其餘:

雙重檢測鎖式(因爲JVM底層內部模型緣由,偶爾會出問題。不建議使用)

靜態內部類式(線程安全,調用效率高。 可是,能夠延時加載)

枚舉式(線程安全,調用效率高,不能延時加載。而且能夠自然的防止反射和反序列化漏洞!)

如何選用?

單例對象 佔用 資源 少,不須要 延時加載:

枚舉式 好於 餓漢式

單例對象 佔用 資源 大,須要 延時加載:

靜態內部類式 好於 懶漢式

相關文章
相關標籤/搜索