一、單例模式能幹啥?
所謂單例,就是整個程序有且僅有一個實例。
某個類全局只有一個實例對象有什麼好處?一方面,由於單例模式只生成一個實例,減少了系統性能開銷;另一方面,單例模式存在全局訪問點,所以可以優化共享資源訪問。比如:網站的計數器,一般也是採用單例模式實現,如果存在多個計數器對象,每一個用戶的訪問都刷新不同的計數器對象的值,統計總數的時候就很麻煩。如果採用單例模式實現就不會存在這樣的問題,而且還可以避免線程安全問題。還有很多這樣的場景:
- Windows的任務管理器
- Windows的回收站,也是一個單例應用
- 項目中的讀取配置文件的對象(如Mybatis中的Configuration類的對象,保存配置信息)
- 數據庫的連接池
- Servlet中的Application Servlet
- Spring中的Bean默認也是單例的
- SpringMVC Struts中的控制器
總的來說適用於:
- 1.需要生成唯一序列的環境
- 2.需要頻繁實例化然後銷燬的對象。
- 3.創建對象時耗時過多或者耗資源過多,但又經常用到的對象。
- 4.方便資源相互通信的環境
二、單例模式常見形式
總的按對象的創建時機,可以分爲懶漢式和餓漢式:
1.餓漢式:線程安全 調用率高 但是不能延遲加載
2.懶漢式:線程安全 調用率不高 但是可以延遲加載
3.雙重檢測(double check )-----懶漢式
4.靜態內部類(線程安全 可以延遲加載)--------懶漢式
5.枚舉單例 線程安全 不可以延遲加載
看着形式很多,實際上單例模式都有以下特點:
- 構造器私有:這個很好理解,要是構造器不私有,那就可以在外部隨意創建不同的對象了,違背了單例思想
- 持有自己類型的屬性
- 對外提供獲取實例的靜態方法
三、懶漢式
懶漢式顧名思義,比較懶,只在你需要的時候才創建這個唯一的對象。它分爲兩種:
懶漢式(1)
public class Singleton1 {
// 自己持有自己
private static Singleton1 instance;
// 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
private Singleton1() {}
// 對外提供獲取唯一實例的靜態方法
public static Singleton1 getInstance() {
// 先判斷實例對象是否已經存在
if(instance == null){
// 創建實例
instance = new Singleton1();
}
return instance;
}
}
上面的如果是單線程的情況下是沒問題的,但是在多線程時就有可能產生多個不同的對象,即是線程不安全的。
懶漢式(2)
上面懶漢(1)線程不安全,因此在創建實例方法上加上鎖就有了下面的
public class Singleton2 {
// 自己持有自己
private static Singleton2 instance;
// 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
private Singleton2() {}
// 對外提供獲取唯一實例的靜態方法
public static synchronized Singleton2 getInstance() {
// 先判斷實例對象是否已經存在
if(instance == null){
// 創建實例
instance = new Singleton2();
}
return instance;
}
}
因爲鎖的原因,效率自然不高。
四、餓漢式
餓漢式顧名思義,比較飢渴,不管你要不要這個對象都先給你創建出來。
public class Singleton3 {
// 自己持有自己並直接創建對象
private static Singleton3 instance = new Singleton3();
// 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
private Singleton3() {}
// 對外提供獲取唯一實例的靜態方法
public static Singleton3 getInstance() {
return instance;
}
}
和懶漢式的延時創建相比,餓漢式在加載類的時候對象就已經創建了,所以加載類的速度比較慢,但是獲取對象的速度比較快,調用效率高,且是線程安全的。
五、雙檢鎖式(DCL)
上面的懶漢式(2)加了鎖雖然線程安全了,但是效率也降低了,而雙檢鎖式可以兼顧線程安全和效率。實際上,雙檢鎖也可以看成是懶漢式的一種特殊形式,也是延時創建唯一的對象。
public class Singleton4 {
// 自己持有自己並直接創建對象(使用volatile關鍵字防止重排序,new Instance()是一個非原子操作,可能創建一個不完整的實例)
private static volatile Singleton4 instance;
// 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
private Singleton4() {}
// 對外提供獲取唯一實例的靜態方法
public static Singleton4 getInstance() {
// 判斷是否存在單例
if(instance == null){
// 加鎖,保持只有一個線程執行(只需在第一次創建實例時才同步)
synchronized (Singleton4.class){
// 再次判斷單例是否被創建(防止其他線程已經創建而導致再次創建)
if (instance == null){
instance = new Singleton4();
}
}
}
return instance;
}
}
這裏有兩次對象的判空處理,鎖是在兩次判空中間的,這個鎖只會在第一次創建實例的時候執行一次,一旦該實例被創建後面的線程就不會再需要拿到這個鎖,因此不會因爲鎖而降低效率。
注意:
這裏還有個關鍵字volatile,它是來防止某個線程獲取不完整對象的。new Instance()是一個非原子操作,jvm存在亂序執行功能,因爲重排序的原因,可能創建出來的就是一個不完整的實例。比如演示一下new Instance()過程:
- 分配實例所需的內存
- 創建引用,指向這個內存
- 初始化實例對象
這是一種順序,但這個順序不是固定的,實際過程中也可能是下面的順序
- 分配實例所需的內存
- 初始化實例對象
- 創建引用,指向這個內存
每次執行順序是重排序的,那就可能發生下面的現象:
- 線程A先進入Singleton4()方法
- 此時instance == null,線程A進入synchronized 塊
- 再次判斷,此時instance == null,線程A在執行 instance = new Singleton4();時,先執行“分配實例所需的內存”和“創建引用指向這個內存”,但還沒有初始化這個對象
- 此時線程B進來,因爲instance雖然未被初始化,但已經非null,不會再進行下面的創建語句,直接返回這個未初始化的instance
- 線程A等到資源後,繼續完成對象初始化操作,線程A獲得完成的instance對象
上面的線程B就拿到了非完整對象,這應該是個重大bug,volatile 關鍵字修飾這個對象可以避免以上重排序可能帶來的問題(volatile 需要在JDK1.5之後的版本才能確保安全)。
六、靜態內部類式
/**
* Feng, Ge 2020/2/29 14:14
*/
public class Singleton5 {
// 構造器私有化,不讓外部通過構造器產生對象,從而保證對象全局唯一
private Singleton5() {}
// 靜態內部類
private static class SingleInnerHolder{
private static Singleton5 instance = new Singleton5();
}
// 對外提供獲取唯一實例的靜態方法
public static Singleton5 getInstance() {
return SingleInnerHolder.instance;
}
}
你肯定覺得這不是餓漢式麼,對象直接就創建了,實際這個對象也是延時創建的,因此也可以理解成是懶漢式的一種特殊形式。
外部類加載時並不需要立即加載內部類,內部類不被加載則不去初始化INSTANCE,故而不佔內存。
這裏當Singleton5類被加載時,其靜態內部類SingletonHolder沒有被主動使用,只有當調用getInstance方法時, 纔會裝載SingletonHolder類,從而實例化單例對象。
這樣的方式既能實現懶漢式對象的延時創建,也保證了線程的安全。
那麼,靜態內部類又是如何實現線程安全的呢?首先,我們先了解下類的加載時機。
類加載時機:JAVA虛擬機在有且僅有的5種場景下會對類進行初始化:
- 遇到new、getstatic、setstatic或者invokestatic這4個字節碼指令時,對應的java代碼場景爲:new一個關鍵字或者一個實例化對象時、讀取或設置一個靜態字段時(final修飾、已在編譯期把結果放入常量池的除外)、調用一個類的靜態方法時。
- 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒進行初始化,需要先調用其初始化方法進行初始化。
- 當初始化一個類時,如果其父類還未進行初始化,會先觸發其父類的初始化。
- 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的類),虛擬機會先初始化這個類。
- 當使用JDK 1.7等動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
這5種情況被稱爲是類的主動引用,注意,這裏《虛擬機規範》中使用的限定詞是"有且僅有",那麼,除此之外的所有引用類都不會對類進行初始化,稱爲被動引用。靜態內部類就屬於被動引用的行列。
我們再回頭看下getInstance()方法,調用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler裏的INSTANCE對象,跟上面那個DCL方法不同的是,getInstance()方法並沒有多次去new對象,故不管多少個線程去調用getInstance()方法,取的都是同一個INSTANCE對象,而不用去重新創建。當getInstance()方法被調用時,SingleTonHoler纔在SingleTon的運行時常量池裏,把符號引用替換爲直接引用,這時靜態對象INSTANCE也真正被創建,然後再被getInstance()方法返回出去,這點同餓漢模式。那麼INSTANCE在創建過程中又是如何保證線程安全的呢?在《深入理解JAVA虛擬機》中,有這麼一句話:
虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個進程阻塞(需要注意的是,其他線程雖然會被阻塞,但如果執行<clinit>()方法後,其他線程喚醒之後不會再次進入<clinit>()方法。同一個加載器下,一個類型只會初始化一次。),在實際應用中,這種阻塞往往是很隱蔽的。
故而,可以看出INSTANCE在創建過程中是線程安全的,所以說靜態內部類形式的單例可保證線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。
那麼,是不是可以說靜態內部類單例就是最完美的單例模式了呢?其實不然,靜態內部類也有着一個致命的缺點,就是傳參的問題,由於是靜態內部類的形式去創建單例的,故外部無法傳遞參數進去,例如Context這種參數,所以,我們創建單例時,可以在靜態內部類與DCL模式裏自己斟酌。
七、枚舉式
public enum Singleton6 {
// 定義1個枚舉的元素,即爲該類的唯一實例
INSTANCE;
public void anyMethod(){
System.out.println("任何一個方法!");
}
}
/**
* Feng, Ge 2020/2/29 15:34
*/
public class TestEnum {
public static void main(String[] args) {
Singleton6 singleton6 = Singleton6.INSTANCE;
singleton6.anyMethod();
}
}
枚舉在java中與普通類一樣,都能擁有字段與方法,防止反序列化生成多個實例,在任何情況下,它都是一個單例,因此線程安全。