錘鍊"單例"

此篇文章不斷更新中, 包括根據java發展, 網絡資源, 博客評論直接做修改, 以便其他讀者不用去扒各地資源, 因爲柔和了思想, 紛雜的片段, 無法一個個註明參考處, 請不要驚訝或氣憤, 由衷感謝相關博客和評論.

單例是設計模式的一種, 從語義上來說就是一個應用內或者一個進程內或者一個系統內, 某個類有且只有一個實例或對象給外部使用, 比如代表文件系統的對象, 全局配置管理器的對象應該保證只有一個, 有些重量級的可重複利用的大對象如果夠用, 可能也會使用單例以減少內存佔用. 不過單例可能很簡單, 也可能很複雜, 可能無法繼承(比如使用枚舉實現)或被繼承(只能內部類或靜態內部類繼承, 但已不符合里氏替換原則吧? 做成final類豈不更好?), 從而變得更加面向過程, 所以如果可能, 且不是出於現實模型的語義, 不應優先考慮使用單例. 在現在的測試驅動開發中, 單例模式由於難以被模擬其行爲而被視爲反模式(anti pattern), 所以如果你是測試驅動開發的開發者, 最好避免使用單例模式, 至少你可能希望單例類實現一個可以充當其類型的接口. 如果存在N多個大對象的單例, 因爲幾乎無法被垃圾回收, 有可能導致內存問題. 傳聞程序員雜誌出過一期, 說GoF覺得單例模式導致很多代碼”壞味道”, 打算在新書中刪除這種模式, 當然權威的歸權威, 市場的歸市場.

最初的單例實現是從初始化時機和應對多線程這兩個方面展開的, 看下面的示例:

public final class Settings {
    public static final Settings SINGLE_INSTANCE = new Settings();
    private Settings() {}
}

這種實現沒有考慮延遲初始化, 但獲取實例的操作天然能應對多線程. 這是最簡單的實現(未必是最差的實現), 如果你確信該類將永遠是單例, 使用這種方式是有意義的, 如果你希望保留餘地, 比如後續你可能在獲取實例的問題上添加權限, 你不想實例赤裸裸的暴露, 亦或者你認爲使用靜態工廠方法是比較嚴謹妥當的處理方式, 甚至將來你可能希望根據需求做成非單例, 那麼你可能這樣寫:

public final class Settings {
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        //check something here or not
        return singleInstance;
    }
    private Settings() {}
}

這裏將該靜態方法聲明爲final, 不是必須的, 可能僅僅出於習慣, 但是你也許不希望內部子類遮蔽隱藏這個方法, 所以聲明爲final不會令你損失更多, 也沒有什麼可值得爭吵的. 有的人會認爲方法聲明爲final將被內聯到所有調用處, 在java的早期版本確實是通過這樣的, 但目前來看, 這僅僅是給jvm一個提示而已, 現在的jvm變得更加自主, 多數情況靜態工廠方法都會被內聯. 或者它本身就不應該被繼承:

public final class Settings {
    private static final Settings singleInstance = new Settings();
    public static Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {}
}

這裏注意, 不要依賴類初始化機制, 像靜態代碼塊往往使問題變得更加複雜, 而且絕大多數情況可以使用靜態方法(這裏是靜態工廠方法)代替靜態代碼塊, 所以除非你已深知這裏可能的陷阱, 並且某些原因使你不得不這麼做, 否則請不要這麼做, 儘管它看起來沒有你想的那麼危險:

public final class Settings {
    private static Settings singleInstance;
    static {
        // maybe do something here
        singleInstance = new Settings();
        // maybe do something here
    }
    public static Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {}
}

上面這幾種實現被稱爲”飢漢式”, 你可以注意到它由三部分組成, 一個私有構造, 一個私有常量域指向使用私有構造生成的實例, 和一個總是返回這個實例的靜態工廠方法. 這樣這個實例看起來就是唯一的了. 過去因爲平臺的限制, 很多用得着用不着的類實例一開始就被加載會拖累啓動運行速度, 而且多佔着部分內存, 所以這種”飢漢式”方式不被提倡, 目前來看, 隨着平臺處理速度的飛速已經軟件工程的發展, 這種方式簡單易用, 避免了很多問題, 不失爲是好的選擇, 尤其是你看到下面那些需要自己應對多線程的實現時, 不過有個缺點很明顯, 就是這種方式無法用在帶參構造, 依賴參數的情況.

既然有”飢漢式”就會有”懶漢式”, 就是實例的初始化延遲到第一次真正使用該實例對象時. 重申一次, 在大多數時候, 正常的初始化要優於延遲初始化. “懶漢式”主要用於構造複雜的大對象或帶參構造的情況以及對性能有特定要求的情況, 讓我們來看看它的演變:

public final class Settings {
    private static Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            singleInstance = new Settings();
        }
        return singleInstance;
    }
    private Settings() {}
}

顯然上面的代碼在Settings完成構造比較耗時的情況下存在多線程問題, 所以可以加個同步:

public final class Settings {
    private static Settings singleInstance;
    public static final synchronized Settings getSingleInstance() {
        if (singleInstance == null) {
            singleInstance = new Settings();
        }
        return singleInstance;
    }
    private Settings() {}
}

但是這樣每次都獲取實例都要鎖一下, 會產生很多不必要的性能損耗, 儘管你可能說如果jvm確定不會產生多線程訪問, 在優化時會做自動鎖消除, 但有太多外界代碼可以干擾jvm的這種”確定”, 進而你也不應該假設這種不確定的”確定”. 爲了解決這個問題, 可以使用爭議頗多的”double-check-lock”:

public final class Settings {
    private static Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            synchronized (Settings.class) {
                if (singleInstance == null) {
                    singleInstance = new Settings();
                }
            }
        }
        return singleInstance;
    }
    private Settings() {}
}

因爲問題出在當單例實例未構造好的情況下, 如果第二個線程訪問, 此時因爲賦值語句還沒執行, 所以還是null, 導致第二個實例被建. 所以加完鎖後再判斷一次, 因爲加完鎖就是串行執行, 而一旦實例已經建好, 也不會走加鎖那一條分支.

但至少在java中, 因爲構造實例並賦值這個操作不是原子的, 它被分割成多條虛擬機指令, 且java語言規範和虛擬機規範中並沒有強制這幾條指令的順序, 即Java編譯器允許處理器亂序執行(out-of-order),以及JDK1.5之前JMM(Java Memory Model)中Cache、寄存器到主內存回寫順序的規定, 所以在亂序中, 可能存在先賦值後構造實例, 具體來說, 首先是分配內存, 正常情況下第二步應該是調用構造器, 然後賦值, 然而也可能是先將變量引用指針指向分配的內存(賦值), 然後在調用構造方法, 怎麼執行可能取決於虛擬機實現和優化方案. 不過確定的是理論上在多線程的情況下, 有可能第二個線程使用未完成構造的”半成品”.

JDK1.5之後,官方已經注意到這種問題,因此調整了JMM、具體化了volatile關鍵字(參見JSR-133內存模型規範或Java語言規範), 可以說保證了在使用volatile時, 所有的寫(write)都將先行發生於讀(read), 不會打亂單線程內構造賦值這一段的指令執行順序, 可參考一下這裏http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html (如果鏈接無效, 請見諒). 而JDK1.5前, volatile使變量可見, 立刻看到了賦值, 加劇了問題重現的概率, 儘管我從未試出這種情況. 故在JDK1.5及以上, 可以安全的使用下面的實現, 不過volatile或多或少也會影響到性能:

public final class Settings {
    private static volatile Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            synchronized (Settings.class) {
                if (singleInstance == null) {
                    singleInstance = new Settings();
                }
            }
        }
        return singleInstance;
    }
    private Settings() {}
}

或者這樣寫也行:

public final class Settings {
    private static volatile Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance != null) {
            return singleInstance;
        }
        synchronized (Settings.class) {
            if (singleInstance == null) {
                singleInstance = new Settings();
            }
            return singleInstance; //return in synchronized block
        }
    }
    private Settings() {}
}

那JDK1.5之前怎麼辦, 有人推薦使用內聯機制避免上述的指令無序的bug. 比如這樣解決:

public final class Settings {
    private static volatile Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            synchronized (Settings.class) {
                if (singleInstance == null) {
                    singleInstance = createInstance();
                }
            }
        }
        return singleInstance;
    }
    private static final Settings createInstance() {
        return new Settings(); //maybe inline
    }
    private Settings() {}
}

最新的jvm在優化上更自主, 內聯不在依賴任何關鍵字特徵(比如final), 但一般靜態工廠方法容易得到內聯, 這部分的資料我從哪裏見過, 不過理由已經忘了. 也許, 只能是也許, 這在JDK1.5前會是個說得過去的方法, 前提是這些發行版本中內聯是確定的, 內聯對指令順序也是確定的.
還有人提出這樣的解決方案:

public final class Settings {
    private static Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            Settings settings;
            synchronized (Settings.class) {
                settings = singleInstance;
                if (settings == null) {
                    synchronized (Settings.class) {
                        settings = new Settings();
                    } // release inner synchronization lock
                }
                singleInstance = settings;
            }
        }
        return singleInstance;
    }
    private Settings() {}
} 

這段代碼將Settings對象的構造放在了內部裏層的synchronized同步塊中, 並賦值給臨時變量, 期望這個同步鎖的釋放位置存在內存屏障, 且能阻止構造初始化指令和賦值分配指令間的重排序, 從而對臨時變量settings的操作不會影響singleInstance, 直到”singleInstance = settings;”被執行之前, singleInstance還是null. 但是它是不會正確工作的, 儘管邏輯上構造和賦值之間的指令重排序已經不會影響用戶使用”半成品”, 但它存在可見性問題, 更重要的是, 同步塊的釋放保證在此之前–也就是同步塊裏面–的操作必須完成, 但是並不保證同步塊之後的操作不能因編譯器優化而調換到同步塊結束之前進行. 就是說編譯器完全可以把”singleInstance = settings;”這句移到內部同步塊裏面執行. 鑑於篇幅就不多說了, 這種方式就不應該使用, 如果還不理解, 至少我是參考的這個: http://www.cs.umd.edu/~pugh/java/memoryModel/BidirectionalMemoryBarrier.html(如果鏈接無效, 請見諒), 不妨您也看看.

其實也可以不上鎖, 不依賴JDK版本解決線程安全和延遲初始化這兩個問題, 就是利用靜態內部類被主動使用時才初始化加載的機制實現”懶漢式”單例:

public final class Settings {
    private static final class SettingsHolder {
        private static final Settings singleInstance = new Settings();
    }
    public static final Settings getSingleInstance() {
        return SettingsHolder.singleInstance;
    }
    private Settings() {}
}

那怎麼辦到線程安全的呢? 這種方式本質上並沒有解決指令重排序的問題, 而是採用了另一種思路, 就是不讓其他線程看到指令重排序的變化來規避了問題. 什麼意思呢?
Java語言規範規定, 對於每一個類或接口, 都有一個唯一的初始化鎖與之對應. 至於怎麼對應由JVM自由實現. 多個線程可能在同一時間嘗試去初始化同一個類或接口, 未避免重複初始化, 誰第一個獲得初始化鎖就成爲構造線程, 其他線程只能等待構造完成, 儘管其他線程也會獲得一次初始化鎖, 以確保這個類已經被初始化過了. 構造線程執行類的靜態初始化, 並初始化類中的靜態屬性, 其實這裏也會涉及賦值和調用構造器的順序無序的問題, 但是因爲其他線程還在那等着呢, 看到了這個變化也只能表示無力. 所以是線程安全的, 更細緻的過程描述, 可參考http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization (如果鏈接有效, 請歡呼).
雖然這種方式沒有多線程問題, 也沒有提前初始化, 彌補了上面幾種實現的弊端, 但是它也有自己的弊端:(1)不適於帶參構造的情況, 如果非要在初始化中使用參數, 只能使用成員方法, 然後團隊做一下約定;(2)不易於單元測試, 這在java web中不是個好消息, 因爲無法注入通過靜態類方法來獲取到的對象的mock;

接下來再看一種《Effective Java》作者, 大師級人物Josh Bloch提倡的方式, 就是使用枚舉類型:

public enum Settings {
    SINGLE_INSTANCE;
    //Any code same as in class here
}

java中enum枚舉類型其實是一個繼承自java.lang.Enum< T >的, 且不可被繼承的final class. 裏面有一個public static final T INSTANCE常量域, T指枚舉名, 這個常量域通過static代碼塊初始化, 被賦予一個new出來的T類型實例, 且不管你在聲明枚舉的時候是否定義了構造器(包括帶參構造器), 其構造器都會被聲明爲private, 所以枚舉變量其實就是類的靜態常量, 此外, 這個類裏會替你定義一個private static final Single[] $VALUES, 一個靜態公開的values()和valueOf(String)方法, 而且編譯器替你把關這個類的實例化和序列化, 哪怕在enum靜態內部類裏都無法new這個枚舉實例. 每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的,
另外Java的序列化規範中指出: 在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中, 反序列化的時候則是通過java.lang.Enum的valueOf(Class, String)方法來根據名字查找枚舉對象, 所以還是那一個. 同時, 編譯器是不允許任何對這種序列化機制的定製的, 因此禁用了writeObject, readObject, readObjectNoData, writeReplace和readResolve等方法(詳細可查看http://docs.oracle.com/javase/1.5.0/docs/guide/serialization/spec/serialTOC.html). 且如果已經序列化某個枚舉, 對它的任何改變(增刪枚舉變量)都可能導致反序列化失敗, 這恰恰給實現單例提供了方便;
它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象, 寫法也簡單. 但是它的缺點是無法用於帶參構造, 無法繼承其他類(可以實現其他接口), 而且enum是在JDK1.5才加入的, 之前版本無法使用, 所以用的人比較少, 對於初級選手不容易一目瞭然的認出這就是單例;

另外我在網上搜到了一些別出心裁的實現方式, 比如下面這種:

public final class ConcurrentMapSingleton {
    private static final ConcurrentMap<String, ConcurrentMapSingleton > singlePool = new ConcurrentHashMap<>();
    private static volatile ConcurrentMapSingleton singleInstance;
    public static final ConcurrentMapSingleton getSingleInstance() {
        if (singleInstance == null) {
            singleInstance = singlePool.putIfAbsent("SINGLE_INSTANCE", new ConcurrentMapSingleton());
        }
        return singleInstance;
    }
}

有人稱它爲”登記式”單例, 再比如下面這個:

public final class AtomicBooleanSingleton {
    private static final AtomicBoolean initialized = new AtomicBoolean(false);
    private static volatile AtomicBooleanSingleton singleInstance;
    public static AtomicBooleanSingleton getSingleInstantce() {
        checkInitialized();
        return singleInstance;
    }
    private static final void checkInitialized() {
        if(singleInstance == null && initialized.compareAndSet(false, true)) {
            singleInstance = new AtomicBooleanSingleton();
        }
    }
}

這兩個實現是藉助併發庫輔助類的優勢, 但依舊有可能會被重複構造多次, 有可能會產生空指針異常, 有可能導致不完全構造的問題, 可能也不夠簡潔, 出乎了你心中對單例模式的想象, 不過這種不斷要求創新的精神還是很值得鼓勵的.

如果你想讓某個類在每個需要使用它的線程都有且僅有一個實例, 可以使用ThreadLocal + double check lock 實現, 這裏是線程安全的, 且延遲了初始化, 不過被爆有性能問題, 運行不快:

public final class PerThreadSingleton {
    private static final ThreadLocal<PerThreadSingleton> perThreadInstances = new ThreadLocal<>();
    private static PerThreadSingleton threadSingleInstance;
    public static final PerThreadSingleton getThreadSingleInstance() {
        if (perThreadInstances.get() == null) {
            createThreadSingleInstance();
        }
        return threadSingleInstance;
    }
    private static final void createThreadSingleInstance() {
        synchronized (PerThreadSingleton.class) {
            if (threadSingleInstance == null) {
                threadSingleInstance = new PerThreadSingleton();
            }
        }
        perThreadInstances.set(threadSingleInstance);
    }
    private PerThreadSingleton() {}
} 

或者這樣(可能重複創建多個實例, 但使用過程中是線程單一實例):

public final class PerThreadSingleton {
    private static final ThreadLocal<PerThreadSingleton> perThreadInstances = new ThreadLocal<>();
    public static final PerThreadSingleton getThreadSingleInstance() {
        if (perThreadInstances.get() == null) {
            perThreadInstances.set(new PerThreadSingleton());
        }
        return perThreadInstances.get();
    }
    private PerThreadSingleton() {}
}

看了這麼多單例的實現方式, 到底應該用那種呢? 我們做開發設計工作的時, 應當既要考慮到需求可能出現的擴展與變化, 也應當避免”幻影需求”導致無謂的提升設計, 實現複雜度, 最終反而帶來工期, 性能和穩定性的損失. 設計不足與設計過度都是危害, 所以說沒有最好的單例模式, 只有最合適上下文的單例模式, 不過理解每種模式出現的原因和解決的問題還是好的.

還沒結束, 單例模式是需要團隊默契和約束的, 如果真的想破壞, 你會發現單例很脆弱, 容易有意無意的被攻破, 導致多個實例產生, 大致有這麼幾個問題需要解決:

  • new出多個實例
  • 單例實現cloneable, 拷貝出多個實例
  • 序列化反序列化出多個實例
  • 反射創建實例
  • 使用多個類加載器加載單例類, 導致多個實例並存

我們現在來解決一下這幾個問題的解決對策:

  1. 只要將類中的所有構造器聲明爲private, 即可避免從該類外部實例化該類, 但你無法避免有些人罔顧你做成單例的意圖, 肆無忌憚的執意在類內部對其實例化, 這個問題只能做團隊約定了, 說句題外話, 個人覺得設計模型, 類層次, 架構是個需要謹慎, 豐富經驗, 影響深遠的事, 但它往往也是賦予創造性和自由的事, 包括你可以引用其他模塊和類庫來搭建你的意志. 不同的, 編碼看起來是個影響範圍很小的事, 但卻該看作是一個嚴如軍令, 程式化的, 重複的, 需要獎懲的事;
  2. 對單例做成可clone的, 是個非常不明智的錯誤, 禁止實現cloneable接口, 不過這個依據是團隊約定的問題;
  3. 爲了維護和保證是單例, 應該避免序列化, 比如使用枚舉方式實現單例, 但如果非要單例類需要實現序列化, 需要把所有的成員屬性聲明爲transient(這樣該字段不會被序列化), 並提供一個readResolve方法返回原單例實例(序列化這塊可查看http://docs.oracle.com/javase/1.5.0/docs/guide/serialization/spec/serialTOC.html). 這裏以簡潔的”餓漢式”爲例:
public final class Settings implements Serializable {
    private static final long serialVersionUID = 6825273283542226860L;
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {}
    // use transient like following
    private transient String id;
    // ... other transient property or field
    private Object readResolve() throws ObjectStreamException {
        // NOTICE: The method is defined as follows: 
        // ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
        return getSingleInstance();
    }
}
  1. 抵禦反射創建實例, 如果是枚舉方式實現, 可忽略這個問題, 如果聲明瞭私有構造器, 可以在構造器中檢查, 在它被要求創建第二個實例時拋出異常, 像下面這樣:
public final class Settings implements Serializable {
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {
        if (singleInstance != null) {
            throw new IllegalStateException("attempt to create second instances");
        }
        // maybe other code here
    }
}

或者如果你的團隊有自己的全局安全權限檢查策略, 且厭惡了暴力反射, 可以考慮下面這種方式, 下面的只是demo, 必須根據實際情況修改才能使用, 亦或者如果你在開發服務端應用, 應用服務器可能提供相關的後臺配置, 那就需要你瞭解這些配置了, 另外這種方式再Android中不起作用, 因爲Android中SecurityManager是個空殼, 沒有實現. 再次強調, 以下的僅作爲參考思路:

public final class Settings {
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {}
    private String id = "main";
    public String getId() {
        return id;
    }
    public static void main(String[] args) throws Exception {
        //if use app server, it may be offer security policy
        SecurityManager securityManager = new SecurityManager();
        System.setSecurityManager(securityManager);
        securityManager.checkPermission(new ReflectPermission("suppressAccessChecks"));

        Class<?> clazz = Class.forName("Settings");
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Settings settings = (Settings) constructor.newInstance();
        System.out.println(settings.getId());
    }
}
  1. 這裏不考慮遠程, 因爲時候情況下, 一旦涉及遠程, 意味着你根本無法控制全部風險. 所以僅限在一個jvm內. 對於抵禦多個類加載器的攻擊, 使用枚舉方式實現單例是沒有用的, 而且我的認知是, 涉及多個類加載器必然使用反射方式, 只不過這次不是調用私有構造器, 而是調用公開的靜態工廠方法獲取新實例. 主要的思路有兩個: (1)只允許一個類加載器加載; (2)不管多少類加載器加載, 仍能返回同一實例; 顯然後者難度較大, 實現複雜, 這裏也有兩個方向, 一個是預先由一個類加載器加載, 其他類加載器加載時尋找預先加載的類實例, 這樣其實與(1)的思路有重疊的地方, 但是怎麼讓其他類加載器知道被預先加載的事已經超出了單例實現的範疇, 你可能爲此需要自定義一個類加載器並查找已經加載的類; 另一個方向是使用動態代理, 並僅暴露代理類, 這樣顯然會給使用過程帶來不便, 而且代碼寫起來也不簡單, 理論上確實是可行的, 不過由於才疏學淺我自己沒試出來, 靈感來自http://surguy.net/articles/communication-across-classloaders.xml. 所以這裏主要還是考慮(1).
    如果只允許一個類加載器加載, 可以在構造器(提倡)或靜態代碼塊(不提倡)裏檢查類加載器是否是系統類加載器實例或者是否是你指定的自定義類加載器類型,如果不是則拋出異常. 如果您還要更好的方式, 請不吝賜教. 下面還是使用”餓漢式”爲例:
public final class Settings {
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {
        if ((Settings.class.getClassLoader() != ClassLoader.getSystemClassLoader())) {
            throw new IllegalStateException("only system class loader can load and init me");
        }
    }
}

現在問題差不多解決完了, 看起來單例真的可以很複雜, 所以開頭就建議慎用單例, 對於像管理者角色的類或者定製全局的類使用單例還是有意義, 甚至是必要的, 比起靜態類也更爲合適, 這裏的靜態類不是指內部靜態類, 也不指作爲utils工具角色的, 或者抽象工廠角色的, 裏面全是靜態方法的類, 而是指直接使用類本身靜態域作爲狀態, 不使用實例對象的類, 當然這裏的方法也都是靜態方法才能操縱修改靜態域. 比起單例, 更應慎用靜態類, 它有如下劣勢: 更爲面向過程, 結構化編程; 無法實現接口, 無法享受多態和繼承覆寫的好處(可以繼承靜態類, 可以重載); 無法延遲初始化, 對於複雜初始化要靠靜態代碼塊, 靜態代碼塊容易產生各種坑; 比單例更難於模擬, 不方便單元測試.

最後談談我實踐單例的心得, 比較有用的單例實現有靜態工廠的”餓漢”, double-check-lock的”懶漢”, 靜態內部類, 枚舉, 併發Map做”登記” 這幾種, 少嗎? 不少, 不過還是那句話, 沒有最好, 只有最適合, 如果全瞭解每一種實現及其演變, 並知道各自利弊, 還能在應用場景找到最適合, 甚至在此基礎上創造出最適合的, 那就更好了! 但大多數人都有偏好, 他們大多習慣並一如既往的只使用一種, 或許是因爲他們對這一種的優劣更瞭然於胸(只應該用自己熟悉的, 以避免不必要的風險). 問題是這種情況下, 你看到一種不熟悉的單例而又沒有認出怎麼辦? 嗚呼, 還有很多種實現版本都沒有列舉, 比如如果你決定全程使用反射實現單例, 誰能攔得了? 顯然這種不能被提倡, 所以團隊做出約定, 應該用什麼樣的方式實現, 能否稍稍自由? 剛剛解決上面的問題很多也都依賴團隊約定, 其實絕大多數情況下不會有人那麼極端, 但是在長期維護和兼容問題的壓力下, 無意識的犯錯不會存在? 所以我提倡這樣一個想法:(帶冒號的)”聲明式編程”.

聲明式編程是新興語言倡導的, 它包括支持函數式編程的語言和DSL, 比如groovy, scalar, python等, java8也已然爲此做出了很多努力. 不過我說的這個範圍更廣, 也更模糊, 我把凡是在不造成性能可觀損失的前提下, 更直觀的顯示代碼要做什麼, 而不是怎麼做, 要充當什麼角色和履行什麼職責, 以及直觀規避風險, 降低學習成本和維護成本, 這方面的努力都作爲”聲明式編程”的實踐. 比如java爲了保證兼容性, 無論是語言本身還是類庫, 都有各種坑和陷阱, 這些實踐至少希望幫助減少這些項目風險, 不過我的微薄努力以後另起新文再談吧. 把相關單例的部分說一下, 你可以定義一個註解@Singleton, 標明這個類是單例實現, 團隊成員看到這個註解就不應該破壞它原有的意圖(當然如果你確信可以改變實現或者廢棄這個類). 這樣就完了? 不, 你還可以像Lint那樣通過這個註解檢查代碼有哪些問題, 比如繼承Cloneable時給出警告, 類內有其他實例創建語句時給出警告, 沒有使用volatile做double-check-lock給出警告, 非私有構造給出警告, 等等. 但是我們不使用反射, 而是在源代碼級別做分析檢查. 那怎麼做源碼分析呢?
最好是藉助IDE的優勢提供自定義plugin, 比如如果團隊成員是用eclipse開發, 可以基於org.eclipse.jdt.core.jar分析源代碼, 如果是intellij IDEA開發, 可以從idea.jar中com.intellij.psi和com.intellij.lang着手, 不過這兩個IDE都不是小項目, 縷清思路找到一個合適的下手點需要耗費些時日. 還有就是參考checkstyle或者PMD(注: Findbugs, JDepend是基於類文件而不是源文件), 這種靜態源代碼分析工具的思路, 他們都是基於AST(Abstract Syntax Tree)語法解析工具, 其中checkstyle是基於ANTLR, PMD是參考JavaCC(注: 其實javaparser項目也是基於JavaCC). 也可以使用我採用的方式, 就是通過javac tool(包括rt.jar和tools.jar, 注: tools.jar和glassfish都集成了codemodel項目去生成源碼)完成, 參考網址是http://docs.oracle.com/javase/8/docs/ 選javac看API Specification. 如果採用的註解生成器呢, 可以使用javapoet提高生成java文件的效率.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章