設計模式學習(二):單例模式

設計模式學習(二):單例模式

作者:Grey

原文地址:

博客園:設計模式學習(二):單例模式

CSDN:設計模式學習(二):單例模式

單例模式

單例模式是創建型模式。

單例的定義:「一個類只允許創建唯一一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。」定義中提到,「一個類只允許創建唯一一個對象」。那對象的唯一性的作用範圍是指進程內只允許創建一個對象,也就是說,單例模式創建的對象是進程唯一的(而非線程)

image

為什麼要使用單例

  1. 處理資源訪問衝突,比如寫日誌的類,如果不使用單例,就必須使用鎖機制來解決日誌被覆蓋的問題。

  2. 表示全局唯一類,比如配置信息類,在系統中,只有一個配置文件,當配置文件加載到內存中,以對象形式存在,也理所應當只有一份;唯一 ID 生成器也是類似的機制。如果程序中有兩個對象,那就會存在生成重複 ID 的情況,所以,我們應該將 ID 生成器類設計為單例。

餓漢式

類加載的時候就會初始化這個實例,JVM 保證唯一實例,線程安全,但是可以通過反射破壞

方式一

public class Singleton1 {
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

方式二

public class Singleton2 {
    private static final Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }
    private Singleton2() {
     
    }
    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

注意:

這種方式不支持延遲加載,如果實例佔用資源多(比如佔用內存多)或初始化耗時長(比如需要加載各種配置文件),提前初始化實例是一種浪費資源的行為。最好的方法應該在用到的時候再去初始化。不過,如果初始化耗時長,那最好不要等到真正要用它的時候,才去執行這個耗時長的初始化過程,這會影響到系統的性能,我們可以將耗時的初始化操作,提前到程序啟動的時候完成,這樣就能避免在程序運行的時候,再去初始化導致的性能問題。如果實例佔用資源多,按照 fail-fast 的設計原則(有問題及早暴露),那我們也希望在程序啟動時就將這個實例初始化好。如果資源不夠,就會在程序啟動的時候觸發報錯(比如 Java 中的 PermGen Space OOM ),我們可以立即去修復。這樣也能避免在程序運行一段時間後,突然因為初始化這個實例佔用資源過多,導致系統崩潰,影響系統的可用性。

這兩種方式都可以通過反射方式破壞,例如:

Class<?> aClass=Class.forName("singleton.Singleton2",true,Thread.currentThread().getContextClassLoader());
Singleton2 instance1=(Singleton2)aClass.newInstance();
Singleton2 instance2=(Singleton2)aClass.newInstance();
System.out.println(instance1==instance2);

懶漢式

雖然可以實現按需初始化,但是線程不安全, 因為在判斷 INSTANCE == null 的時候,有可能出現一個線程還沒有把 INSTANCE初始化好,另外一個線程判斷 INSTANCE==null 得到 true,就會繼續初始化

public class Singleton3 {
    private static Singleton3 INSTANCE;

    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        if (INSTANCE == null) {
            // 模擬初始化對象需要的耗時操作
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}

為了防止線程不安全,可以在 getInstance 方法上加鎖,這樣既實現了按需初始化,又保證了線程安全,

但是加鎖可能會導致一些性能的問題:我們給 getInstance()這個方法加了一把大鎖,導致這個函數的並發度很低。量化一下的話,並發度是 1,也就相當於串行操作了。如果這個單例類偶爾會被用到,那這種實現方式還可以接受。但是,如果頻繁地用到,那頻繁加鎖、釋放鎖及並發度低等問題,會導致性能瓶頸,這種實現方式就不可取了。

public class Singleton4 {
    private static Singleton4 INSTANCE;

    private Singleton4() {
    }

    public static synchronized Singleton4 getInstance() {
        if (INSTANCE == null) {
            // 模擬初始化對象需要的耗時操作
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton4();
        }
        return INSTANCE;
    }
}

為了提升一點點性能,可以不給 getInstance() 整個方法加鎖,而是對 INSTANCE 判空這段代碼加鎖, 但是這樣一來又帶來了線程不安全的問題

public class Singleton5 {
    private static Singleton5 INSTANCE;

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton5.class) {
                // 模擬初始化對象需要的耗時操作
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }
}

Double Check Locking 模式,就是雙加鎖檢查模式,這種方式中,volatile 關鍵字是必需的,目的為了防止指令重排,生成一個半初始化的的實例,導致生成兩個實例。

具體可參考 雙重檢索(DCL)的思考: 為什麼要加volatile?

public class Singleton6 {
    private volatile static Singleton6 INSTANCE;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton6.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

以下兩種更為優雅的方式,既保證了線程安全,又實現了按需加載。

方式一:靜態內部類方式, JVM 保證單例,加載外部類時不會加載內部類,這樣可以實現懶加載

public class Singleton7 {
    private Singleton7() {
    }

    public static Singleton7 getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final Singleton7 INSTANCE = new Singleton7();
    }

}

方式二: 使用枚舉, 這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止多次實例化,這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化。

public enum Singleton8 {
    INSTANCE;
}

單例模式的替代方案

使用靜態方法

   // 靜態方法實現方式
public class IdGenerator {
    private static AtomicLong id = new AtomicLong(0);
   
    public static long getId() { 
       return id.incrementAndGet();
    }
}

// 使用舉例
long id = IdGenerator.getId();

使用依賴注入

   // 1. 老的使用方式
   public demofunction() {
     //...
     long id = IdGenerator.getInstance().getId();
     //...
   }
   
   // 2. 新的使用方式:依賴注入
   public demofunction(IdGenerator idGenerator) {
     long id = idGenerator.getId();
   }
   // 外部調用demofunction()的時候,傳入idGenerator
   IdGenerator idGenerator = IdGenerator.getInsance();
   demofunction(idGenerator);

線程單例

通過一個 HashMap 來存儲對象,其中 key 是線程 ID,value 是對象。這樣我們就可以做到,不同的線程對應不同的對象,同一個線程只能對應一個對象。實際上,Java 語言本身提供了 ThreadLocal 工具類,可以更加輕鬆地實現線程唯一單例。不過,ThreadLocal 底層實現原理也是基於下面代碼中所示的 HashMap 。


public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);

  private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>();

  private IdGenerator() {}

  public static IdGenerator getInstance() {
    Long currentThreadId = Thread.currentThread().getId();
    instances.putIfAbsent(currentThreadId, new IdGenerator());
    return instances.get(currentThreadId);
  }

  public long getId() {
    return id.incrementAndGet();
  }
}

集群模式下單例

集群模式下如果要實現單例需要把這個單例對象序列化並存儲到外部共享存儲區(比如文件)。進程在使用這個單例對象的時候,需要先從外部共享存儲區中將它讀取到內存,並反序列化成對象,然後再使用,使用完成之後還需要再存儲回外部共享存儲區。為了保證任何時刻,在進程間都只有一份對象存在,一個進程在獲取到對象之後,需要對對象加鎖,避免其他進程再將其獲取。在進程使用完這個對象之後,還需要顯式地將對象從內存中刪除,並且釋放對對象的加鎖。

如何實現一個多例模式

「單例」指的是一個類只能創建一個對象。對應地,「多例」指的就是一個類可以創建多個對象,但是個數是有限制的,比如只能創建 3 個對象。多例的實現也比較簡單,通過一個 Map 來存儲對象類型和對象之間的對應關係,來控制對象的個數。

單例模式的應用舉例

JDK 的 Runtime 類

public class Runtime {
  private static Runtime currentRuntime = new Runtime();

  public static Runtime getRuntime() {
    return currentRuntime;
  }
  
  /** Don't let anyone else instantiate this class */
  private Runtime() {}
.......
}

還有就是 Spring 中 AbstractBeanFactory 中包含的兩個功能。

功能一,就是從緩存中獲取單例 Bean

功能二,就是從 Bean 的實例中獲取對象。

UML 和 代碼

UML 圖

代碼

更多

設計模式學習專欄

參考資料