多线程并发下的单例模式
定義:
單例模式是設計模式中最簡單的形式之一。這一模式的目的是使得類的一個對象成為系統中的唯一實例。
下面通過代碼分析下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、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????
點擊這里快速進入簡書
總結
以上是生活随笔為你收集整理的多线程并发下的单例模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入分析JVM逃逸分析对性能的影响
- 下一篇: 局部变量和常量的性能分析