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()是非原子操作的,主要包含三個過程
- 給instance分配記憶體
- 呼叫建構函式初始化instance
- 將instance指向分配的空間(instance指向分配空間後,instance就不為空了)
雖然synchronized塊保證了只有一個執行緒進入同步塊,但是在同步塊內部JVM出於優化需要可能進行指令重排,例如(1->3->2),instance還沒有初始化之前其他執行緒就會在外部檢查到instance不為null,而返回還沒有初始化的instance,從而造成邏輯錯誤。
- volatile保證變數的可見性
volatile型別變數可以保證寫入對於讀取的可見性,JVM不會將volatile變數上的操作與其他記憶體操作一起重新排序,volatile變數不會被快取在暫存器,因此保證了檢測instance狀態時總是檢測到instance的最新狀態。
- volatile保證變數的可見性
注意: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();
}
}
}
單例模式效能總結
方式 | 優點 | 缺點 |
---|---|---|
餓漢式 | 執行緒安全, 呼叫效率高 | 不能延遲載入 |
懶漢式 | 執行緒安全, 可以延遲載入 | 呼叫效率不高 |
雙重檢測鎖式 | 執行緒安全, 呼叫效率高, 可以延遲載入 | - |
靜態內部類式 | 執行緒安全, 呼叫效率高, 可以延遲載入 | - |
列舉單例 | 執行緒安全, 呼叫效率高 | 不能延遲載入 |
單例效能測試
測試結果:
- HungerSingleton 共耗時: 30 毫秒
- LazySingleton 共耗時: 48 毫秒
- DoubleCheckSingleton 共耗時: 25 毫秒
- StaticInnerSingleton 共耗時: 16 毫秒
- 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{} 靜態初始化塊會在類載入過程中執行;
{} 則只是在物件初始化過程中執行, 但先於構造器;
內部類
內部類訪問許可權
- Java 外部類只有兩種訪問許可權:public/default, 而內部類則有四種訪問許可權:private/default/protected/public. 而且內部類還可以使用static修飾;內部類可以擁有private訪問許可權、protected訪問許可權、public訪問許可權及包訪問許可權。如果成員內部類Inner用private修飾,則只能在外部類的內部訪問,如果用public修飾,則任何地方都能訪問;如果用protected修飾,則只能在同一個包下或者繼承外部類的情況下訪問;如果是預設訪問許可權,則只能在同一個包下訪問。這一點和外部類有一點不一樣,外部類只能被public和包訪問兩種許可權修飾。成員內部類可以看做是外部類的一個成員,所以可以像類的成員一樣擁有多種許可權修飾。
- 內部類分為成員內部類與區域性內部類, 相對來說成員內部類用途更廣泛, 區域性內部類用的較少(匿名內部類除外), 成員內部類又分為靜態(static)內部類與非靜態內部類, 這兩種成員內部類同樣要遵守static與非static的約束(如static內部類不能訪問外部類的非靜態成員等)
非靜態內部類
- 非靜態內部類在外部類內使用時, 與平時使用的普通類沒有太大區別;
- Java不允許在非static內部類中定義static成員,除非是static final的常量型別
- 如果外部類成員變數, 內部類成員變數與內部類中的方法裡面的區域性變數有重名, 則可通過this, 外部類名.this加以區分.
- 非靜態內部類的成員可以訪問外部類的private成員, 但反之不成立, 內部類的成員不被外部類所感知. 如果外部類需要訪問內部類中的private成員, 必須顯示建立內部類例項, 而且內部類的private許可權對外部類也是不起作用的:
靜態內部類
- 使用static修飾內部類, 則該內部類隸屬於該外部類本身, 而不屬於外部類的某個物件.
- 由於static的作用, 靜態內部類不能訪問外部類的例項成員, 而反之不然;
匿名內部類
如果(方法)區域性變數需要被匿名內部類訪問, 那麼該區域性變數需要使用final修飾.
列舉
- 列舉類繼承了java.lang.Enum, 而不是Object, 因此列舉不能顯示繼承其他類; 其中Enum實現了Serializable和Comparable介面(implements Comparable, Serializable);
- 非抽象的列舉類預設使用final修飾,因此列舉類不能派生子類;
- 列舉類的所有例項必須在列舉類的第一行顯示列出(列舉類不能通過new來建立物件); 並且這些例項預設/且只能是public static final的;
- 列舉類的構造器預設/且只能是private;
- 列舉類通常應該設計成不可變類, 因此建議成員變數都用private final修飾;
- 列舉類不能使用abstract關鍵字將列舉類宣告成抽象類(因為列舉類不允許有子類), 但如果列舉類裡面有抽象方法, 或者列舉類實現了某個介面, 則定義每個列舉值時必須為抽象方法提供實現,