定義:
單例模式是設計模式中最簡單的形式之一。這一模式的目的是使得類的一個對象成爲系統中的唯一實例。
下面通過代碼分析下java中,各種單例模式寫法的優缺點。
1、餓漢模式
示例1.1
public class Singleton {
private Singleton() {}
private static Object INSTANCE = new Object();
public static Object getInstance() {
return INSTANCE;
}
}
在類生命週期的【初始化】階段進行生成單例對象(類的初始化階段會對靜態變量賦值),當執行類初始化的階段是需要先獲得鎖才能進行初始化操作,而且一個class類只進行初始化一次。類初始化階段是線程安全的,JVM保證類初始化只執行一次。這樣可以確保只生成一個對象。
類聲明週期分爲:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸御(Unloading)。
類的生命週期不明白的請查看:JVM 類加載機制深入淺出
類加載後不一定馬上執行初始化階段。當遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。
- new 創建對象操作
- getstatic 訪問類的靜態變量操作
- putstatic 給類的靜態變量賦值操作
- invokestatic 調用靜態方法操作
這個餓漢模式中,不會出現new、invokestatic和putstatic指令,外面的類只能調用 getInstance()靜態方法,由此推斷,此單例模式也是延遲加載對象的,只有第一次調用getInstance()靜態方法,纔會觸發他的初始化階段,纔會創建單例對象。
其實這個例子應該是懶漢模式,只有在第一次使用的時候才加載
下面這個【示例1.2】不是延遲加載單例對象
示例1.2
public class Singleton {
private Singleton() {}
private static int count=0;
private static Object INSTANCE = new Object();
public static Object getInstance() {
return INSTANCE;
}
}
當程序先調用Singleton1中的count屬性時(getstatic 或putstatic 指令),就會執行類的【初始化】階段,會生成單例對象,而不是調用getInstance()靜態方法才生成單例對象。
示例1.3 (靜態內部類實現方式)
public class Singleton {
private Singleton() {}
private static int count=0;
private static class SingletonHolder{
private static final Object INSTANCE = new Object();
}
public static Object getInstance(){
return SingletonHolder.INSTANCE;
}
}
使用內部類SingletonHolder來防止【示例1.2】出現的問題,防止其它的變量的干擾,導致提前觸發類聲明週期中的【初始化】階段來創建INSTANCE 實例。
Effective Java中推薦的單例寫法
2、懶漢模式
示例2.1
public class Singleton{
private Singleton() { }
private static Object INSTANCE = null;
public static Object getInstance() {
if(INSTANCE == null){
INSTANCE = new Object();
}
return INSTANCE;
}
}
每次創建INSTANCE 的時候先判斷是否null,如果爲null則new一個,否則就直接返回INSTANCE 。當多線程工作的時候,如果有多個線程同時運行到if (INSTANCE == null),都判斷爲null,那麼兩個線程就各自會創建一個實例。這樣就會創建多一個實例,這樣就不是單例了。
下面的【示例2.2】加上synchronized 改進多線程併發引起的問題
示例2.2 (synchronized 實現方式)
public class Singleton {
private Singleton() { }
private static Object INSTANCE = null;
public synchronized static Object getInstance() {
if(INSTANCE == null){
INSTANCE = new Object();
}
return INSTANCE;
}
}
雖然synchronized 能解決多線程同時併發引起的問題,但是每次訪問該方法都需要獲得鎖,性能大大降低。其實只要創建INSTANCE 實例後就不需要加鎖的,直接獲取該對象就ok。
示例2.3 (雙重檢查實現方式)
public class Singleton {
private Singleton() { }
private static Object INSTANCE = null;
public static Object getInstance() {
if(INSTANCE == null){
synchronized(Singleton3.class){
if(INSTANCE == null){
INSTANCE = new Object();
}
}
}
return INSTANCE;
}
}
這個版本的代碼看起來有點複雜,注意其中有兩次if (instance == null)的判斷,這個叫做『雙重檢查 Double-Check』。
第一個if (instance == null),其實是爲了解決【示例2.2】中的效率問題,只有instance爲null的時候,才進入synchronized的代碼段——這樣在對象創建後就不會在進入同步代碼塊了。
第二個if (instance == null),則是跟【示例2.2】一樣,是爲了防止可能出現多個實例的情況。
從代碼層面看似完美,效率問題也解決了。但實際還是有問題,在併發環境下可能會出現instance爲null的情況。下面我們來分析下爲什麼會出現此問題。
原子操作
INSTANCE = new Object();不是原子操作。
在JVM中會拆分成3個步驟
1、分配對象的內存空間
2、初始化對象
3、設置INSTANCE 指向剛分配的內存地址
指令重排
指令重排序是JVM爲了優化指令,提高程序運行效率,在不影響單線程程序執行結果的前提下,儘可能地提高並行度。
可以參考:java內存模型
【2、初始化對象和 3、設置INSTANCE 指向剛分配的內存地址】這兩個操作可能發生重排序。
如下圖:
從圖中可以看出A2和A3的重排序,將導致線程
B在B1處判斷出instance不爲空,線程B接下來將訪問instance引用的對象。此時,線程B將會訪
問到一個還未初始化的對象。
示例2.4 (基於volatile的解決方案)
public class Singleton {
private Singleton() {}
private static volatile Object INSTANCE = null;
public static Object getInstance() {
if(INSTANCE == null){
synchronized(Singleton.class){
if(INSTANCE == null){
INSTANCE = new Object();
}
}
}
return INSTANCE;
}
}
聲明對象的引用爲volatile後,【2、初始化對象和 3、設置INSTANCE 指向剛分配的內存地址】之間的重排序,在多線程環境中將會被禁止。
從圖表中可以看出volatile可以確保,volatile變量讀寫順序,可以保證一個線程寫volatile變量完成後(創建完對象後),其它線程才能讀取該volatile變量,相當於給這個創建實例的構造上了一把鎖。這樣,在它的賦值完成之前,就不用會調用讀操作。
示例2.5 (枚舉實現方式)
public enum Singleton6 {
INSTANCE;
public String getInfo(String s){
s = "hello " + s;
System.out.println(s);
return s;
}
public static void main(String[] args) {
String s = INSTANCE.getInfo("aa");
System.out.println(s);
}
}
這種寫法在功能上與共有域方法相近,但是它更簡潔,無償地提供了序列化機制,絕對防止對此實例化,即使是在面對複雜的序列化或者反射攻擊的時候。雖然這中方法還沒有廣泛採用,但是單元素的枚舉類型已經成爲實現Singleton的最佳方法。
本人簡書blog地址:http://www.jianshu.com/u/1f0067e24ff8
點擊這裏快速進入簡書