摘要:一般來說,這種單例實現有兩種思路,私有構造器,枚舉。而這種方式又分了飽漢式,餓漢式。通過關鍵字防止指令重排序。
什么是單例?為什么要用單例?一個類被設計出來,就代表它表示具有某種行為(方法),屬性(成員變量),而一般情況下,當我們想使用這個類時,會使用new關鍵字,這時候jvm會幫我們構造一個該類的實例。而我們知道,對于new這個關鍵字以及該實例,相對而言是比較耗費資源的。所以如果我們能夠想辦法在jvm啟動時就new好,或者在某一次實例new好以后,以后不再需要這樣的動作,就能夠節省很多資源了。
哪些類可以使用單例?一般而言,我們總是希望無狀態的類能夠設計成單例,那這個無狀態代表什么呢? 簡單而言,對于同一個實例,如果多個線程同時使用,并且不使用額外的線程同步手段,不會出現線程同步的問題,我們就可以認為是無狀態的,再簡單點:一個類沒有成員變量,或者它的成員變量也是無狀態的,我們就可以考慮設計成單例。
實現方法好了,我們已經知道什么是單例,為什么要使用單例了,那我們接下來繼續討論下怎么實現單例。 一般來說,我們可以把單例分為行為上的單例和管理上的單例。行為上的單例代表不管如何操作(此處不談cloneable,反射),至始至終jvm中都只有一個類的實例,而管理上的單例則可以理解為:不管誰去使用這個類,都要守一定的規矩,比方說,我們使用某個類,只能從指定的地方’去拿‘,這樣拿到就是同一個類了。 而對于管理上的單例,相信大家最為熟悉的就是spring了,spring將所有的類放到一個容器中,以后使用該類都從該容器去取,這樣就保證了單例。 所以這里我們剩下的就是接著來談談如何實現行為上的單例了。一般來說,這種單例實現有兩種思路,私有構造器,枚舉。
枚舉實現單例枚舉實現單例是最為推薦的一種方法,因為就算通過序列化,反射等也沒辦法破壞單例性,例子:
public enum SingletonEnum {
INSTANCE;
public static void main(String[] args) {
System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
}
}
結果自然是true,而如果我們嘗試使用反射破壞單例性:
public enum BadSingletonEnum {
/**
*
*/
INSTANCE;
public static void main(String[] args) throws Exception{
System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);
Constructor badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor();
badSingletonEnumConstructor.setAccessible(true);
BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance();
System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
}
}
結果如下:
Exception in thread "main" java.lang.NoSuchMethodException: cn.jsbintask.BadSingletonEnum.()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:18)
異常居然是沒有init方法,這是為什么呢? 那我們反編譯查看下這個枚舉類的字節碼:
// class version 52.0 (52)
// access flags 0x4031
// signature Ljava/lang/Enum;
// declaration: cn/jsbintask/BadSingletonEnum extends java.lang.Enum
public final enum cn/jsbintask/BadSingletonEnum extends java/lang/Enum {
// compiled from: BadSingletonEnum.java
// access flags 0x4019
public final static enum Lcn/jsbintask/BadSingletonEnum; INSTANCE
// access flags 0x101A
private final static synthetic [Lcn/jsbintask/BadSingletonEnum; $VALUES
}
結果發現這個枚舉類繼承了抽象類java.lang.Enum,我們接著看下Enum,發現構造器:
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
那我們接著改變代碼,反射調用這個構造器:
public enum BadSingletonEnum {
/**
*
*/
INSTANCE();
public static void main(String[] args) throws Exception{
System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);
Constructor badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor(String.class, int.class);
badSingletonEnumConstructor.setAccessible(true);
BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance("test", 0);
System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
}
}
結果如下:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:21)
這次雖然方法找到了,但是直接給我們了一句Cannot reflectively create enum objects,不能夠反射創造枚舉對象,接著我們繼續看下**newInstance(...)**這個方法:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<");null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
關鍵代碼就是:if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");,所以就是jdk從根本上拒絕了使用反射去創建(知道為啥java推薦使用enum實現單例了吧),另外,我們再觀察下Enum類的clone和序列化方法,如下:
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can"t deserialize enum");
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can"t deserialize enum");
}
一眼看出,直接丟出異常,不允許這么做!(真親兒子系列)。 所以,結論就是:枚舉是最靠譜的實現單例的方式!
私有構造器另外一個實現單例最普通的方法則是私有構造器,開放獲取實例公共方法,雖然這種方法還是可以用clone,序列化,反射破壞單例性(除非特殊情況,我們不會這么做),但是卻是最容易理解使用的。而這種方式又分了飽漢式,餓漢式。
看名字就知道,饑渴!(咳咳,開個玩笑),它指的是當一個類被jvm加載的時候就會被實例化,這樣可以從根本上解決多個線程的同步問題,例子如下:
public class FullSingleton {
private static FullSingleton ourInstance = new FullSingleton();
public static FullSingleton getInstance() {
return ourInstance;
}
private FullSingleton() {
}
public static void main(String[] args) {
System.out.println(FullSingleton.getInstance() == FullSingleton.getInstance());
}
}
結果自然是true,雖然這種做法很方便的幫我們解決了多線程實例化的問題,但是缺點也很明顯,因為這句代碼**private static FullSingleton ourInstance = new FullSingleton();**的關系,所以該類一旦被jvm加載就會馬上實例化,那如果我們不想用這個類怎么辦呢? 是不是就浪費了呢?既然這樣,我們來看下替代方案! 飽漢式。
既然是飽,就代表它不著急,那我們可以這么寫:
public class HungryUnsafeSingleton {
private static HungryUnsafeSingleton instance;
public static HungryUnsafeSingleton getInstance() {
if (instance == null) {
instance = new HungryUnsafeSingleton();
}
return instance;
}
private HungryUnsafeSingleton() {}
}
用意很容易理解,就是用到**getInstance()**方法才去檢查instance,如果為null,就new一個,這樣就不怕浪費了,但是這個時候問題就來了:現在有這么一種情況,在有兩個線程同時 運行到了 instane == null這個語句,并且都通過了,那他們就會都實例化一個對象,這樣就又不是單例了。既然這樣,哪有什么解決辦法呢? 鎖方法
直接同步方法 這種方法比較干脆利落,那就是直接在getInstance()方法上加鎖,這樣就解決了線程問題:
public class HungrySafeSingleton {
private static HungrySafeSingleton instance;
public static synchronized HungrySafeSingleton getInstance() {
if (instance == null) {
instance = new HungrySafeSingleton();
}
return instance;
}
private HungrySafeSingleton() {
System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
}
public static void main(String[] args) {
System.out.println(HungrySafeSingleton.getInstance() == HungrySafeSingleton.getInstance());
}
}
很簡單,很容易理解,加鎖,只有一個線程能實例該對象。但是,此時問題又來了,我們知道對于靜態方法而言,synchronized關鍵字會鎖住整個 Class,這時候又會有性能問題了(尼瑪墨跡),那有沒有優化的辦法呢? 雙重檢查鎖:
public class HungrySafeSingleton {
private static volatile HungrySafeSingleton instance;
public static HungrySafeSingleton getInstance() {
/* 使用一個本地變量可以提高性能 */
HungrySafeSingleton result = instance;
if (result == null) {
synchronized (HungrySafeSingleton.class) {
result = instance;
if (result == null) {
instance = result = new HungrySafeSingleton();
}
}
}
return result;
}
private HungrySafeSingleton() {
System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
}
public static void main(String[] args) {
System.out.println(HungrySafeSingleton.getInstance() == HungrySafeSingleton.getInstance());
}
}
synchronized關鍵字只加在了關鍵的地方,并且通過本地變量提高了性能(effective java),這樣線程安全并且不浪費資源的單例就完成了。
通過volitalile關鍵字防止指令重排序。 對其他線程可見。
關注我,這里只有干貨!
總結本章,我們一步一步從什么是單例,到為什么要使用單例,再到怎么使用單例,并且從源碼角度分析了為什么枚舉是最適合的實現方式,然后接著講解了飽漢式,餓漢式的寫法以及好處,缺點。 例子源碼:github.com/jsbintask22…
本文原創地址:jsbintask的博客,轉載請注明出處。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/7202.html
摘要:小麗總是會在朋友圈發布自己的各種生活狀態。總結我們從觀察者模式特點入手,通過一個案例,一步一步完善了觀察著的寫法,特點組后介紹了總已有的實現關注我,這里只有干貨同系列文章從未這么明白的設計模式一單例模式 showImg(https://segmentfault.com/img/remote/1460000018874501); 本文原創地址,我的博客:https://jsbintask...
摘要:上面是簡單的單例模式,自己寫程序的話夠用了,如果想繼續延伸,請傳送至大話設計模式之單例模式升級版 看了那么多單例的介紹,都是上來就說怎么做,也沒見說為什么這么做的。那小的就來說說為什么會有單例這個模式以便更好的幫助初學者真正的理解這個設計模式,如果你是大神,也不妨看完指正一下O(∩_∩)O首先我不得不吐槽一下這個模式名字單例,初學者通過字面很難理解什么是單例,我覺得應該叫唯一模式更貼切...
摘要:那有什么辦法保證只有一個領導人斯大林呢較常見的兩種方式餓漢式和懶漢式二實戰圖這里提示一點,在學習設計模式的時候,圖會讓你更容易,而且深刻的去理解到該模式的核心。下一篇的設計模式是工廠方法模式。 ??就算不懂設計模式的兄弟姐妹們,想必也聽說過單例模式,并且在項目中也會用上。但是,真正理解和熟悉單例模式的人有幾個呢?接下來我們一起來學習設計模式中最簡單的模式之一——單例模式 一、為什么叫單...
摘要:本篇文章總結了目前主流的實現單例模式的方法供讀者參考。使用實現單例模式同樣,我們在類的創建時進行干預,從而達到實現單例的目的。 很多初學者喜歡用 全局變量 ,因為這比函數的參數傳來傳去更容易讓人理解。確實在很多場景下用全局變量很方便。不過如果代碼規模增大,并且有多個文件的時候,全局變量就會變得比較混亂。你可能不知道在哪個文件中定義了相同類型甚至重名的全局變量,也不知道這個變量在程序的某...
閱讀 3318·2019-08-29 16:17
閱讀 1975·2019-08-29 15:31
閱讀 2645·2019-08-29 14:09
閱讀 2548·2019-08-26 13:52
閱讀 744·2019-08-26 12:21
閱讀 2125·2019-08-26 12:08
閱讀 991·2019-08-23 17:08
閱讀 1922·2019-08-23 16:59