每天學一點,每天進步一點,質的飛躍需要量的積累,add oil!!!
首先我們學任何一項技術都必須知道其應用的場景,其後我們再去學習它的使用方法
常見應用場景
- Windows的 Task Manager(任務管理器)就是很典型的單例模式
- 項目中,讀取配置文件的類,一般也只有一個對象。沒有必要每次使用配置文件數據,每次new幾個對象去讀取
- 網站的計數器,一般也是採用單例模式實現,否則難以同步。
- 應用程序的日誌應用,一般都何用單例模式實現,這一般是由於共享的日誌文件一直處於打開狀態,因爲只能有一個實例去操作,否則內容不好追加
- 數據庫連接池的設計一般也是採用單例模式,因爲數據庫連接是種數據庫資源。
- 操作系統的文件系統,也是大的單例模式實現的具體例子,一個操作系統只能有一個文件系統
- Application也是單例的典型應用( Servlet編程中會涉及到)
- 在 Spring中,每個Bean默認就是單例的,這樣做的優點是 Spring容器可以管理
- 在 servlet編程中,每個servlet也是單例
- 在 spring MVC框架中,控制器對象也是單例
總結:只需要一個對象來解決問題時需要單例模式。可能有些不理解,可以在之後的學習慢慢體會
爲什麼要學單例模式?
單例模式在筆試面試中經常出現,且項目設計也會用到的設計模式。
下面我們就開始學習單例模式的使用,也是本文關鍵點
常見的五種單例模式實現方式
主要
學習下面兩個可以應付一般的筆試面試
餓漢式(線程安全,調用效率高。但是,不能延時加載。)
懶漢式(線程安全,調用效率不高。但是,可以延時加載。)
其他
下面三點是吊打面試官系列,比較難,面試官都可能不知道。學好了面試加分,好好學哦。
雙重檢測鎖式(由於JVM底層內部模型原因,偶爾會出問題。不建議使用)
靜態內部類式(線程安全,調用效率高。但是,可以延時加載)
枚舉單例(線程安全,調用效率高,不能延時加載,由JVM從根本上提供保障!避免通過反射和反序列化的漏洞)
下面我們逐個擊破
餓漢式
顧名思義,餓漢它飢渴,所以…,直接上代碼來理解
public class Hungry {
//餓漢它飢渴,一來就創建女朋友
//類初始化時,立即加載這個對象女朋友(沒有延時加載的優勢)。加載類時,天然的是線程安全
private static Hungry girlfriend = new Hungry();//注意:private,保證只能通過getInstance()方法來創建實例
private Hungry() {//注意:private,保證只能通過getInstance()方法來創建實例
}
//方法沒有同步,調用效率高!
public static Hungry getInstance() {
return girlfriend;
}
}
public class Test {
public static void main(String[] args) {
Hungry girlfriend1 = Hungry.getInstance();
Hungry girlfriend2 = Hungry.getInstance();
System.out.println(girlfriend1 == girlfriend2);//永遠是true
}
}
總結:
- 線程安全:類加載器加載對象是一個天然的線程安全,所以是線程安全的
- 可能造成資源浪費:如果加載了餓漢類,必然會創建女朋友,那如果這個餓漢又沒有使用這個女朋友,必然會讓其它男生少了一個可以選擇的對象,造成資源的浪費
餓漢式單例模代碼中, static變量會在類裝載時初始化,此時也不會涉及多個線程對象訪問該對象的問題。虛擬機保證只會裝載一次該類,肯定不會發生併發訪問的問題。因此,可以省略 synchronized關鍵字。問題:如果只是加載本類,而不是要調用 getinstance0,甚至永遠沒有調用,則會造成資源浪費!
懶漢式
public class LazyMan {
//懶漢比較懶,沒有找女朋友,等有需求了再找
//類初始化時,不初始化這個對象(延時加載,真正用的時候再創健建)。
private static LazyMan girlfriend;
//私有化構造器
public LazyMan() {
}
//方法同步,調用效率低
public static synchronized LazyMan getInstance(){
if (girlfriend == null) { //防止重複創建實例
girlfriend = new LazyMan();
}
return girlfriend;
}
}
public class Test {
public static void main(String[] args) {
LazyMan girlfriend1 = LazyMan.getInstance();
LazyMan girlfriend2 = LazyMan.getInstance();
System.out.println(girlfriend1 == girlfriend2);//true
}
}
爲什麼要加synchronized 關鍵字?
- 假設有A和B兩個線程,A線程 執行到if (girlfriend == null) 掛起,此時沒有創建對象,在A掛起的時間段內,B線程執行到if (girlfriend == null) ,判斷條件成立,創建對象girlfriend ,此時A線程再次開始執行,又創建了一個對象girlfriend ,於是不滿足單例模式只創建一個對象的條件。所以我們要在LazyMan 類前加鎖。
總結:
- lazy load!延遲加載,懶加載!真正用的時候才加載
- 資源利用率高了。但是,每次調用 getinstance0方法都要同步,併發效率較低
雙重檢測鎖式
public class DoubleCheckLock {
private static DoubleCheckLock instance = null;
public static DoubleCheckLock getInstance() {
if (instance == null) {
DoubleCheckLock sc;
synchronized (DoubleCheckLock.class) {
sc = instance;
if (sc == null) {
synchronized (DoubleCheckLock.class) {
if (sc == null)
sc = new DoubleCheckLock();
}
}
instance = sc;
}
}
return instance;
}
private DoubleCheckLock(){
}
}
總結:
- 這方法知道思想即可,實際工作中不會使用
- 這個模式將同步內容下方到內部,提高了執行的效率, 不必每次獲取對象時都進行同步,只有第一次才同步,創建了以後就沒必要了。
問題: - 由於編譯器優化原醫和JVM底層內部模型原因
- 偶爾會出問題。不建議使用
靜態內部類式
其實也可以理解爲懶加載,此方法重點掌握,框架也經常使用此方法
public class StaticInnerClass {
//定義靜態內部類,初始化StaticInnerClass時,不會加載靜態內部類
private static class SingletonClassInstance{
private static final StaticInnerClass instance = new StaticInnerClass();
}
//不要同步方法,調用效率高,線程安全且高效
public static StaticInnerClass getInstance(){
return SingletonClassInstance.instance;
}
private StaticInnerClass() {
}
}
總結:
- 外部類沒有 static屬性,則不會像餓漢式那樣立即加載對象。
- 只有真正調用 getinstance(),纔會加載靜態內部類。加載類時是線程安全的。
- Instance是 static final類型,保證了內存中只有這樣一個實例存在,而且只能被賦值一次,從而保證了線程安全性
- 兼備了併發高效調用和延遲加載的優勢!
枚舉單例
//使用枚舉實現單例模式
public enum EnumSingleton {
//這個枚舉元素,本身就是單例對象
INSTANCE;
//添加需要的操作
public void singletonOperation(){
}
}
public class Test {
public static void main(String[] args) {
System.out.println(EnumSingleton.INSTANCE == EnumSingleton.INSTANCE);//true
}
}
總結:
- 優點
- 實現簡單
- 枚舉本身就是單例模式。由JVM從根本上提供保障!避免通過反射和反序列化的漏洞
- 缺點
- 無延遲加載
如何選用最適合的方法?
學習了五種方法之後,那麼實際運用中,如何選擇合適的方法呢?
當單例對象佔用資源少,不需要延時加載時
- 枚舉式優於餓漢式
當單例對象佔用資源大,需要延時加載時
- 靜態內部類式優於懶漢式
Java小白修煉手冊