應用場景
由於單例模式只生成一個實例, 減少了系統性能開銷(如: 當一個對象的產生需要比較多的資源時, 如讀取配置, 產生其他依賴對象, 則可以通過在應用啓動時直接產生一個單例對象, 然後永久駐留內存的方式來解決)
- 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並不保證操作的原子性,例如即使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關鍵字將枚舉類聲明成抽象類(因爲枚舉類不允許有子類), 但如果枚舉類裏面有抽象方法, 或者枚舉類實現了某個接口, 則定義每個枚舉值時必須爲抽象方法提供實現,
$(function () {
$('pre.prettyprint code').each(function () {
var lines = $(this).text().split('\n').length;
var $numbering = $('').addClass('pre-numbering').hide();
$(this).addClass('has-numbering').parent().append($numbering);
for (i = 1; i <= lines; i++) {
$numbering.append($('').text(i));
};
$numbering.fadeIn(1700);
});
});