Java單例模式的正確實現
Java單例模式
單例的好處
單例模式適合於應用中頻繁建立的物件,如果是重量級的物件,更應該使用單例模式。比如配置檔案,如果不採用單例模式的話,每個配置檔案物件的內容都是一樣的,建立重複的物件就會浪費寶貴的記憶體,所以有必要使用單例模式,達到效能的提升,減小了記憶體的開銷和GC的壓力。本文會一步一步由淺入深的討論如何實現正確的單例模式。
單例模式的一般寫法
- 餓漢式
public class HungryMode {
private static HungryMode sHungryMode = new HungryMode();
private HungryMode () {
System.out.println("create " + getClass().getSimpleName());
}
public static void fun(){
System.out.println("call fun in HungryMode");
}
public static HungryMode getInstance(){
return sHungryMode;
}
public static void main(String[] args) {
HungryMode.fun();
}
}
餓漢式單例,就是一個私有的構造方法加一個私有的靜態當前類例項物件和一個公有的靜態獲取例項方法組成由於類例項物件為靜態變數,所以在載入類的時候我們就會建立類的例項物件,這樣的話比較消耗記憶體,浪費效能。
可以用HungryMode.fun()方法驗證,在直接呼叫這個方法的時候,會載入HungryMode這個類到記憶體中,並且也會例項話靜態的類例項物件。所以執行效果為
create HungryMode
call fun in HungryMode
如果這個物件的構造方法很複雜的話,這樣的單例寫法會造成類載入很慢,並且會浪費很多效能,所以我們需要懶載入,也就是所謂的懶漢式載入
- 懶漢式
public class LazyMode {
private static LazyMode sLazyMode;
private LazyMode() {
System.out.println("create " + getClass().getSimpleName());
}
public static LazyMode getInstance(){
if (sLazyMode == null) {
sLazyMode = new LazyMode();
}
return sLazyMode;
}
public static void main(String[] args) {
LazyMode.getInstance();
}
}
懶漢式在餓漢式的基礎上做了改進,類例項物件做了懶載入,也就是所謂的延時載入,所以提升了一些效能。
多執行緒的單例
以上的寫法在單執行緒的程式設計環境中沒有什麼問題,但是如果是多個執行緒使用上面的單例模式就會違背單例模式的設計原則,出現多個物件,簡單的做法是在獲取類例項的方法上加同步鎖,並且給類例項物件加上volatile修飾符,volatile能保證物件的可見性,即在工作記憶體的內容更新能立即在主記憶體中可見。工作記憶體是執行緒獨有的記憶體,主記憶體是所有執行緒共享的記憶體。還有一個作用是禁止指令重排序優化。大家知道我們寫的程式碼(尤其是多執行緒程式碼),由於編譯器優化,在實際執行的時候可能與我們編寫的順序不同。編譯器只保證程式執行結果與原始碼相同,卻不保證實際指令的順序與原始碼相同。這在單執行緒看起來沒什麼問題,然而一旦引入多執行緒,這種亂序就可能導致嚴重問題。volatile關鍵字就可以從語義上解決這個問題。如程式碼所示
public class LazyMode {
private static volatile LazyMode sLazyMode;
private LazyMode() {
System.out.println("create " + getClass().getSimpleName());
}
public static LazyMode getInstance(){
synchronized (LazyMode.class) {
if (sLazyMode == null) {
sLazyMode = new LazyMode();
}
}
return sLazyMode;
}
public static void main(String[] args) {
LazyMode.getInstance();
}
}
其實上面的程式碼還是有點效能問題的,因為同步鎖的機制,多個執行緒獲取類例項物件會排隊等待獲取鎖,這樣是沒必要的,因為大多數情況下類例項物件都已經建立成功了,所以不用進入加鎖的程式碼塊,於是就可以再次改進上面的程式碼為雙重校驗的單例模式,如程式碼所示
/**
* 多執行緒的單例模式,使用雙重校驗機制
*/
public class DoubleCheckMode {
private volatile static DoubleCheckMode sDoubleCheckMode ;
public DoubleCheckMode() {
System.out.println(" created " + getClass().getSimpleName());
}
public static DoubleCheckMode getInstance() {
if (sDoubleCheckMode == null)
synchronized (DoubleCheckMode.class) {
if (sDoubleCheckMode == null) {
sDoubleCheckMode = new DoubleCheckMode();
}
}
return sDoubleCheckMode;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
super.run();
System.out.println("thread" + getId());
DoubleCheckMode.getInstance();
}
}.start();
}
}
}
這樣的寫法就能夠做到效率和安全的雙重保證。但是有個問題,禁止指令重排優化這條語義直到jdk1.5以後才能正確工作。此前的JDK中即使將變數宣告為volatile也無法完全避免重排序所導致的問題。所以,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無法保證執行緒安全的。不過現在的jdk環境大多數都是1.5之後的了,這個問題我們有個印象就行了。
使用靜態內部類例項單例
這種實現就是利用靜態類只會載入一次的機制,使用靜態內部類持有單例物件,達到單例的效果,直接上程式碼吧
/**
* 靜態內部類的方式實現單例,可以保證多執行緒的物件唯一性,還有提升效能,不用同步鎖機制
*/
public class InnerStaticMode {
private static class SingleTonHolder {
public static InnerStaticMode sInnerStaticMode = new InnerStaticMode();
}
public static InnerStaticMode getInstance(){
return SingleTonHolder.sInnerStaticMode;
}
}
使用列舉實現單例
/**
* 利用列舉的方式實現單例,Android不推薦
*/
public enum EnumMode {
INSTANCE;
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
總結
列舉實現單例,不推薦在Android平臺使用,因為記憶體消耗會其他方式多一些,Android官方也不推薦列舉,Android平臺推薦雙重校驗或者靜態內部類單例,現在的Android開發環境jdk一般都大於1.5了。所以volatile的問題不必擔心。Java平臺開發的Effective Java一書中推薦使用列舉實現單例,可以保證效率,而且還能解決反序列化建立新物件的問題。