天天看點

java單例模式深度解析

應用場景

由于單例模式隻生成一個執行個體, 減少了系統性能開銷(如: 當一個對象的産生需要比較多的資源時, 如讀取配置, 産生其他依賴對象, 則可以通過在應用啟動時直接産生一個單例對象, 然後永久駐留記憶體的方式來解決)

  • Windows中的任務管理器;
  • 檔案系統, 一個作業系統隻能有一個檔案系統;
  • 資料庫連接配接池的設計與實作;
  • Spring中, 一個Component就隻有一個執行個體Java-Web中, 一個Servlet類隻有一個執行個體;

實作要點

  • 聲明為private來隐藏構造器
  • private static Singleton執行個體
  • 聲明為public來暴露執行個體擷取方法

單例模式主要追求三個方面性能

  • 線程安全
  • 調用效率高
  • 延遲加載

實作方式

主要有五種實作方式,懶漢式(延遲加載,使用時初始化),餓漢式(聲明時初始化),雙重檢查,靜态内部類,枚舉。

懶漢式,線程不安全的實作

由于沒有同步,多個線程可能同時檢測到執行個體沒有初始化而分别初始化,進而破壞單例限制。

public class Singleton {
    private static Singleton instance;
    private Singleton() {
    };
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}             

懶漢式,線程安全但效率低下的實作

由于對象隻需要在初次初始化時需要同步,多數情況下不需要互斥的獲得對象,加鎖會造成巨大無意義的資源消耗

public class Singleton {
    private static Singleton instance;
    private Singleton() {
    };
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}             

雙重檢查

這種方法對比于上面的方法確定了隻有在初始化的時候需要同步,當初始化完成後,再次調用getInstance不會再進入synchronized塊。

NOTE

内部檢查是必要的

由于在同步塊外的if語句中可能有多個線程同時檢測到instance為null,同時想要擷取鎖,是以在進入同步塊後還需要再判斷是否為null,避免因為後續獲得鎖的線程再次對instance進行初始化

instance聲明為volatile類型是必要的。

  • 指令重排

    由于初始化操作 instance=new Singleton()是非原子操作的,主要包含三個過程

    1. 給instance配置設定記憶體
    2. 調用構造函數初始化instance
    3. 将instance指向配置設定的空間(instance指向配置設定空間後,instance就不為空了)

      雖然synchronized塊保證了隻有一個線程進入同步塊,但是在同步塊内部JVM出于優化需要可能進行指令重排,例如(1->3->2),instance還沒有初始化之前其他線程就會在外部檢查到instance不為null,而傳回還沒有初始化的instance,進而造成邏輯錯誤。

      • volatile保證變量的可見性

        volatile類型變量可以保證寫入對于讀取的可見性,JVM不會将volatile變量上的操作與其他記憶體操作一起重新排序,volatile變量不會被緩存在寄存器,是以保證了檢測instance狀态時總是檢測到instance的最新狀态。

注意:volatile并不保證操作的原子性,例如即使count聲明為volatile類型,count++操作被分解為讀取->寫入兩個操作,雖然讀取到的是count的最新值,但并不能保證讀取與寫入之間不會有其他線程再次寫入,進而造成邏輯錯誤

public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {
    };
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}             

餓漢式

這種方式基于單ClassLoder機制,instance在類加載時進行初始化,避免了同步問題。餓漢式的優勢在于實作簡單,劣勢在于不是懶加載模式(lazy initialization)

  • 在需要執行個體之前就完成了初始化,在單例較多的情況下,會造成記憶體占用,加載速度慢問題
  • 由于在調用getInstance()之前就完成了初始化,如果需要給getInstance()函數傳入參數,将會無法實作
public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() {
    };
    public static Singleton getInstance() {
        return instance;
    }
}             

靜态内部類

由于内部類不會在類的外部被使用,是以隻有在調用getInstance()方法時才會被加載。同時依賴JVM的ClassLoader類加載機制保證了不會出現同步問題。

public class Singleton {
    private Singleton() {
    };
    public static Singleton getInstance() {
        return Holder.instance;
    }
    private static class Holder{
        private static Singleton instance = new Singleton();
    }
}             

枚舉方法

參見枚舉類解析

- 線程安全

由于枚舉類的會在編譯期編譯為繼承自java.lang.Enum的類,其構造函數為私有,不能再建立枚舉對象,枚舉對象的聲明和初始化都是在static塊中,是以由JVM的ClassLoader機制保證了線程的安全性。但是不能實作延遲加載

- 序列化

由于枚舉類型采用了特殊的序列化方法,進而保證了在一個JVM中隻能有一個執行個體。

  • 枚舉類的執行個體都是static的,且存在于一個數組中,可以用values()方法擷取該數組
  • 在序列化時,隻輸出代表枚舉類型的名字屬性 name
  • 反序列化時,根據名字在靜态的數組中查找對應的枚舉對象,由于沒有建立新的對象,因而保證了一個JVM中隻有一個對象
public enum Singleton {
    INSTANCE;
    public String error(){
        return "error";
    }
}            

單例模式的破壞與防禦

反射

對于枚舉類,該破解方法不适用。

import java.lang.reflect.Constructor;
public class TestCase {
    public void testBreak() throws Exception {
        Class<Singleton> clazz = (Class<Singleton>) Class.forName("Singleton");
        Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton instance1 = constructor.newInstance();
        Singleton instance2 = constructor.newInstance();
        System.out.println("singleton? " + (instance1 == instance2));
    }
    public static void main(String[] args) throws Exception{
        new TestCase().testBreak();
    }
}             

序列化

對于枚舉類,該破解方法不适用。

該測試首先需要聲明Singleton為實作了可序列化接口

public class Singleton implements Serializable

public class TestCase {
    private static final String SYSTEM_FILE = "save.txt";
    public void testBreak() throws Exception {
        Singleton instance1 = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SYSTEM_FILE));
        oos.writeObject(instance1);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SYSTEM_FILE));
        Singleton instance2 = (Singleton) ois.readObject();
        System.out.println("singleton? " + (instance1 == instance2));
    }
    public static void main(String[] args) throws Exception{
        new TestCase().testBreak();
    }
}             

ClassLoader

JVM中存在兩種ClassLoader,啟動内裝載器(bootstrap)和使用者自定義裝載器(user-defined class loader),在一個JVM中可能存在多個ClassLoader,每個ClassLoader擁有自己的NameSpace。一個ClassLoader隻能擁有一個class對象類型的執行個體,但是不同的ClassLoader可能擁有相同的class對象執行個體,這時可能産生緻命的問題。

防禦

對于序列化與反序列化,我們需要添加一個自定義的反序列化方法,使其不再建立對象而是直接傳回已有執行個體,就可以保證單例模式。

我們再次用下面的類進行測試,就發現結果為true。

public final class Singleton {
    private Singleton() {
    }
    private static final Singleton INSTANCE = new Singleton();
    public static Singleton getInstance() {
        return INSTANCE;
    }
    private Object readResolve() throws ObjectStreamException {
        // instead of the object we're on,
        // return the class variable INSTANCE
        return INSTANCE;
    }
public class TestCase {
    private static final String SYSTEM_FILE = "save.txt";
    public void testBreak() throws Exception {
        Singleton instance1 = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SYSTEM_FILE));
        oos.writeObject(instance1);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SYSTEM_FILE));
        Singleton instance2 = (Singleton) ois.readObject();
        System.out.println("singleton? " + (instance1 == instance2));
    }
    public static void main(String[] args) throws Exception {
        new TestCase().testBreak();
    }
}  
}             

單例模式性能總結

方式 優點 缺點
線程安全, 調用效率高 不能延遲加載
懶漢式 線程安全, 可以延遲加載 調用效率不高
雙重檢測鎖式 線程安全, 調用效率高, 可以延遲加載 -
靜态内部類式
枚舉單例

單例性能測試

測試結果:

  1. HungerSingleton 共耗時: 30 毫秒
  2. LazySingleton 共耗時: 48 毫秒
  3. DoubleCheckSingleton 共耗時: 25 毫秒
  4. StaticInnerSingleton 共耗時: 16 毫秒
  5. EnumSingleton 共耗時: 6 毫秒

在不考慮延遲加載的情況下,枚舉類型獲得了最好的效率,懶漢模式由于每次方法都需要擷取鎖,是以效率最低,靜态内部類與雙重檢查的效果類似。考慮到枚舉可以輕松有效的避免序列化與反射,是以枚舉是較好實作單例模式的方法。

public class TestCase {
    private static final String SYSTEM_FILE = "save.txt";
    private static final int THREAD_COUNT = 10;
    private static final int CIRCLE_COUNT = 100000;
    public void testSingletonPerformance() throws IOException, InterruptedException {
        final CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        FileWriter writer = new FileWriter(new File(SYSTEM_FILE), true);
        long start = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; ++i) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < CIRCLE_COUNT; ++i) {
                        Object instance = Singleton.getInstance();
                    }
                    latch.countDown();
                }
            }).start();
        }
        latch.await();
        long end = System.currentTimeMillis();
        writer.append("Singleton 共耗時: " + (end - start) + " 毫秒\n");
        writer.close();
    }
    public static void main(String[] args) throws Exception{
        new TestCase().testSingletonPerformance();
    }
}             

補充知識

類加載機制

static關鍵字的作用是把類的成員變成類相關,而不是執行個體相關,static塊會在類首次被用到的時候進行加載,不是對象建立時,是以static塊具有線程安全性

- 普通初始化塊

當Java建立一個對象時, 系統先為對象的所有執行個體變量配置設定記憶體(前提是該類已經被加載過了), 然後開始對這些執行個體變量進行初始化, 順序是: 先執行初始化塊或聲明執行個體變量時指定的初始值(這兩處執行的順序與他們在源代碼中排列順序相同), 再執行構造器裡指定的初始值.

  • 靜态初始化塊

    又名類初始化塊(普通初始化塊負責對象初始化, 類初始化塊負責對類進行初始化). 靜态初始化塊是類相關的, 系統将在類初始化階段靜态初始化, 而不是在建立對象時才執行. 是以靜态初始化塊總是先于普通初始化塊執行.

  • 執行順序

    系統在類初始化以及對象初始化時, 不僅會執行本類的初始化塊[static/non-static], 而且還會一直上溯到java.lang.Object類, 先執行Object類中的初始化塊[static/non-static], 然後執行其父類的, 最後是自己.

    頂層類(初始化塊, 構造器) -> … -> 父類(初始化塊, 構造器) -> 本類(初始化塊, 構造器)

  • 小結

    static{} 靜态初始化塊會在類加載過程中執行;

    {} 則隻是在對象初始化過程中執行, 但先于構造器;

内部類

  • 内部類通路權限
    1. Java 外部類隻有兩種通路權限:public/default, 而内部類則有四種通路權限:private/default/protected/public. 而且内部類還可以使用static修飾;内部類可以擁有private通路權限、protected通路權限、public通路權限及包通路權限。如果成員内部類Inner用private修飾,則隻能在外部類的内部通路,如果用public修飾,則任何地方都能通路;如果用protected修飾,則隻能在同一個包下或者繼承外部類的情況下通路;如果是預設通路權限,則隻能在同一個包下通路。這一點和外部類有一點不一樣,外部類隻能被public和包通路兩種權限修飾。成員内部類可以看做是外部類的一個成員,是以可以像類的成員一樣擁有多種權限修飾。
    2. 内部類分為成員内部類與局部内部類, 相對來說成員内部類用途更廣泛, 局部内部類用的較少(匿名内部類除外), 成員内部類又分為靜态(static)内部類與非靜态内部類, 這兩種成員内部類同樣要遵守static與非static的限制(如static内部類不能通路外部類的非靜态成員等)
  • 非靜态内部類
    1. 非靜态内部類在外部類内使用時, 與平時使用的普通類沒有太大差別;
    2. Java不允許在非static内部類中定義static成員,除非是static final的常量類型
    3. 如果外部類成員變量, 内部類成員變量與内部類中的方法裡面的局部變量有重名, 則可通過this, 外部類名.this加以區分.
    4. 非靜态内部類的成員可以通路外部類的private成員, 但反之不成立, 内部類的成員不被外部類所感覺. 如果外部類需要通路内部類中的private成員, 必須顯示建立内部類執行個體, 而且内部類的private權限對外部類也是不起作用的:
    1. 使用static修飾内部類, 則該内部類隸屬于該外部類本身, 而不屬于外部類的某個對象.
    2. 由于static的作用, 靜态内部類不能通路外部類的執行個體成員, 而反之不然;
  • 匿名内部類

    如果(方法)局部變量需要被匿名内部類通路, 那麼該局部變量需要使用final修飾.

枚舉

  1. 枚舉類繼承了java.lang.Enum, 而不是Object, 是以枚舉不能顯示繼承其他類; 其中Enum實作了Serializable和Comparable接口(implements Comparable, Serializable);
  2. 非抽象的枚舉類預設使用final修飾,是以枚舉類不能派生子類;
  3. 枚舉類的所有執行個體必須在枚舉類的第一行顯示列出(枚舉類不能通過new來建立對象); 并且這些執行個體預設/且隻能是public static final的;
  4. 枚舉類的構造器預設/且隻能是private;
  5. 枚舉類通常應該設計成不可變類, 是以建議成員變量都用private final修飾;
  6. 枚舉類不能使用abstract關鍵字将枚舉類聲明成抽象類(因為枚舉類不允許有子類), 但如果枚舉類裡面有抽象方法, 或者枚舉類實作了某個接口, 則定義每個枚舉值時必須為抽象方法提供實作,