深入理解幾種單例模式的實現方式


餓漢式

餓漢式的單例實現方式就是說在類加載的時候就已經創建並初始化好了,所以實例的創建過程是線程安全的

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static final IdGenerator instance = new IdGenerator();
    private IdGenerator(){}
    public static IdGenerator getInstance(){
        return instance;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

但是餓漢式是有一個缺點的,它不支持延時加載,也就是說實例在使用之前就已經創建好了,如果占用資源過多,但又不使用,會造成一定的浪費,還是需要根據具體情況確定要不要使用這種方式。不過也有人說,如果耗時長,那在使用的時候再加載,就會影響性能,也是難為了餓漢了。

AtomicLong是一個原子變量類型,提供了線程安全的原子操作,這樣可以保證在多線程環境下,獲取id的時候不會出現重復ID的情況。

代碼中的構造函數通過private修飾符進行修飾,保證了外部代碼不能通過構造函數初始化IdGenerator類。

上面是一個簡單的唯一遞增ID號碼生成器,采用的餓漢式的單例實現模式,instance實例定義成了一個靜態常量,我們這知道在運行一個類的時候,先要加載到JVM中,類加載的過程分為三個階段,分別是:加載、鏈接和初始化,其中鏈接階段又分為三個步驟分別是:驗證、准備和解析,其中在准備的步驟中就會創建類或接口的靜態變量,並初始化靜態變量的初始值。

懶漢式

上面我們說到餓漢式是不支持延時加載到的,那懶漢式就支持延時加載了,懶漢式的實現方式是給獲取實例的方法加了鎖

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance;
    private IdGenerator(){}
    
    public static synchronized IdGenerator getInstance(){
        if (instance == null){
            instance = new IdGenerator();
        }
        return instance;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

加鎖的結果就是性能降低,如果這個單例被頻繁的使用的話,那性能問題就會比較嚴重,需要考慮換種方式實現了。

雙重檢測

雙重檢測的單例實現方式彌補了上面餓漢式和懶漢式的缺點:不能延時加載和性能低的問題,具體實現方式就是在獲取的實例的時候先判斷是否已經創建過了,如果是就直接返回,這是第一重檢測,沒有的話,就進入同步塊,進入同步塊后再進行判斷實例是否已經存在,如果存在就直接返回,這是第二重檢測,如果不存在的話就在同步的情況下創建一個實例。

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance;
    private IdGenerator(){
        // 初始化代碼
    }
    public static IdGenerator getInstance(){
        if (instance == null){
            synchronized (IdGenerator.class){ // 這里指明synchronized保護的是當前的類對象
                if (instance == null){
                    instance = new IdGenerator();
                }
            }
        }
        return instance;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

懶漢式的實現方式每次獲取實例的時候都要進同步代碼,這樣就會造成多次的獲取鎖釋放鎖,造成性能損耗,但是雙重檢測實際上只需要同步一次創建實例就可以了,再獲取實例的時候是不用進同步塊代碼的,這樣就大大提高了性能。

靜態內部類

靜態內部類的實現方式是一種比雙重檢測更加簡單的一種實現方式,而且既保證了性能又做到了延時加載

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private IdGenerator(){
        // 初始化代碼
    }
    private static class SingletonHolder{
        private static final IdGenerator instance = new IdGenerator();
    }
    
    public static IdGenerator getInstance(){
        return SingletonHolder.instance;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

SingletonHolder是一個靜態內部類,使用privat可以使內部類完全對外隱藏,當外部類IdGenerator加載的時候,並不會創建實例,只有當調用getInstance方法的時候才會加載SingletonHolder,並創建實例,這樣就具備了餓漢式的安全特性,同時也具備了延遲加載的特性。

枚舉

枚舉的實現方式是利用了java枚舉類型本身的特性,保證了實例創建的線程安全和實例的唯一性

public enum IdGenerator {
    INSTANCE;
    private AtomicLong id = new AtomicLong(0);
    public long getId(){
        return id.incrementAndGet();
    }
    public static void main(String[] args) {
        IdGeneratorEnum.INSTANCE.getId();
    }
}

可以看出枚舉的實現方式是最簡潔的,由jvm保證線程安全和單一實例。還可以有效防止序列化和反序列化造成多個實例和利用反射創建多個實例的情況。枚舉類型在編譯成class文件后,再反編譯,

public final class IdGenerator extends java.lang.Enum<IdGenerator> {
  public static final IdGenerator INSTANCE;
  public static IdGenerator[] values();
  public static IdGenerator valueOf(java.lang.String);
  public long getId();
  public static void main(java.lang.String[]);
  static {};
}

反編譯后的枚舉類其實繼承了Enum這個類,INSTANCE是一個靜態常量了,似乎又回到了餓漢式的那種那些,但是比它多出了延遲加載而且更加簡潔的特性

歡迎大家去 我的博客 瞅瞅,里面有更多關於測試實戰的內容哦!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM