多线程并发下的单例模式
定義:
單例模式是設(shè)計(jì)模式中最簡(jiǎn)單的形式之一。這一模式的目的是使得類的一個(gè)對(duì)象成為系統(tǒng)中的唯一實(shí)例。
下面通過代碼分析下java中,各種單例模式寫法的優(yōu)缺點(diǎn)。
1、餓漢模式
示例1.1
public class Singleton {private Singleton() {}private static Object INSTANCE = new Object();public static Object getInstance() {return INSTANCE;} }在類生命周期的【初始化】階段進(jìn)行生成單例對(duì)象(類的初始化階段會(huì)對(duì)靜態(tài)變量賦值),當(dāng)執(zhí)行類初始化的階段是需要先獲得鎖才能進(jìn)行初始化操作,而且一個(gè)class類只進(jìn)行初始化一次。類初始化階段是線程安全的,JVM保證類初始化只執(zhí)行一次。這樣可以確保只生成一個(gè)對(duì)象。
類聲明周期分為:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸御(Unloading)。
類的生命周期不明白的請(qǐng)查看:JVM 類加載機(jī)制深入淺出
類加載后不一定馬上執(zhí)行初始化階段。當(dāng)遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
這個(gè)餓漢模式中,不會(huì)出現(xiàn)new、invokestatic和putstatic指令,外面的類只能調(diào)用 getInstance()靜態(tài)方法,由此推斷,此單例模式也是延遲加載對(duì)象的,只有第一次調(diào)用getInstance()靜態(tài)方法,才會(huì)觸發(fā)他的初始化階段,才會(huì)創(chuàng)建單例對(duì)象。
其實(shí)這個(gè)例子應(yīng)該是懶漢模式,只有在第一次使用的時(shí)候才加載
下面這個(gè)【示例1.2】不是延遲加載單例對(duì)象
示例1.2
public class Singleton {private Singleton() {}private static int count=0;private static Object INSTANCE = new Object();public static Object getInstance() {return INSTANCE;} }當(dāng)程序先調(diào)用Singleton1中的count屬性時(shí)(getstatic 或putstatic 指令),就會(huì)執(zhí)行類的【初始化】階段,會(huì)生成單例對(duì)象,而不是調(diào)用getInstance()靜態(tài)方法才生成單例對(duì)象。
示例1.3 (靜態(tài)內(nèi)部類實(shí)現(xiàn)方式)
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;} }使用內(nèi)部類SingletonHolder來(lái)防止【示例1.2】出現(xiàn)的問題,防止其它的變量的干擾,導(dǎo)致提前觸發(fā)類聲明周期中的【初始化】階段來(lái)創(chuàng)建INSTANCE 實(shí)例。
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;} }每次創(chuàng)建INSTANCE 的時(shí)候先判斷是否null,如果為null則new一個(gè),否則就直接返回INSTANCE 。當(dāng)多線程工作的時(shí)候,如果有多個(gè)線程同時(shí)運(yùn)行到if (INSTANCE == null),都判斷為null,那么兩個(gè)線程就各自會(huì)創(chuàng)建一個(gè)實(shí)例。這樣就會(huì)創(chuàng)建多一個(gè)實(shí)例,這樣就不是單例了。
下面的【示例2.2】加上synchronized 改進(jìn)多線程并發(fā)引起的問題
示例2.2 (synchronized 實(shí)現(xiàn)方式)
public class Singleton {private Singleton() { }private static Object INSTANCE = null;public synchronized static Object getInstance() {if(INSTANCE == null){INSTANCE = new Object();}return INSTANCE;} }雖然synchronized 能解決多線程同時(shí)并發(fā)引起的問題,但是每次訪問該方法都需要獲得鎖,性能大大降低。其實(shí)只要?jiǎng)?chuàng)建INSTANCE 實(shí)例后就不需要加鎖的,直接獲取該對(duì)象就ok。
示例2.3 (雙重檢查實(shí)現(xiàn)方式)
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;} }這個(gè)版本的代碼看起來(lái)有點(diǎn)復(fù)雜,注意其中有兩次if (instance == null)的判斷,這個(gè)叫做『雙重檢查 Double-Check』。
第一個(gè)if (instance == null),其實(shí)是為了解決【示例2.2】中的效率問題,只有instance為null的時(shí)候,才進(jìn)入synchronized的代碼段——這樣在對(duì)象創(chuàng)建后就不會(huì)在進(jìn)入同步代碼塊了。
第二個(gè)if (instance == null),則是跟【示例2.2】一樣,是為了防止可能出現(xiàn)多個(gè)實(shí)例的情況。
從代碼層面看似完美,效率問題也解決了。但實(shí)際還是有問題,在并發(fā)環(huán)境下可能會(huì)出現(xiàn)instance為null的情況。下面我們來(lái)分析下為什么會(huì)出現(xiàn)此問題。
原子操作
INSTANCE = new Object();不是原子操作。
在JVM中會(huì)拆分成3個(gè)步驟
1、分配對(duì)象的內(nèi)存空間
2、初始化對(duì)象
3、設(shè)置INSTANCE 指向剛分配的內(nèi)存地址
指令重排
指令重排序是JVM為了優(yōu)化指令,提高程序運(yùn)行效率,在不影響單線程程序執(zhí)行結(jié)果的前提下,盡可能地提高并行度。
可以參考:java內(nèi)存模型
【2、初始化對(duì)象和 3、設(shè)置INSTANCE 指向剛分配的內(nèi)存地址】這兩個(gè)操作可能發(fā)生重排序。
如下圖:
從圖中可以看出A2和A3的重排序,將導(dǎo)致線程
B在B1處判斷出instance不為空,線程B接下來(lái)將訪問instance引用的對(duì)象。此時(shí),線程B將會(huì)訪
問到一個(gè)還未初始化的對(duì)象。
示例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;} }聲明對(duì)象的引用為volatile后,【2、初始化對(duì)象和 3、設(shè)置INSTANCE 指向剛分配的內(nèi)存地址】之間的重排序,在多線程環(huán)境中將會(huì)被禁止。
從圖表中可以看出volatile可以確保,volatile變量讀寫順序,可以保證一個(gè)線程寫volatile變量完成后(創(chuàng)建完對(duì)象后),其它線程才能讀取該volatile變量,相當(dāng)于給這個(gè)創(chuàng)建實(shí)例的構(gòu)造上了一把鎖。這樣,在它的賦值完成之前,就不用會(huì)調(diào)用讀操作。
示例2.5 (枚舉實(shí)現(xiàn)方式)
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);} }這種寫法在功能上與共有域方法相近,但是它更簡(jiǎn)潔,無(wú)償?shù)靥峁┝诵蛄谢瘷C(jī)制,絕對(duì)防止對(duì)此實(shí)例化,即使是在面對(duì)復(fù)雜的序列化或者反射攻擊的時(shí)候。雖然這中方法還沒有廣泛采用,但是單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法。
本人簡(jiǎn)書blog地址:http://www.jianshu.com/u/1f0067e24ff8????
點(diǎn)擊這里快速進(jìn)入簡(jiǎn)書
總結(jié)
以上是生活随笔為你收集整理的多线程并发下的单例模式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入分析JVM逃逸分析对性能的影响
- 下一篇: 局部变量和常量的性能分析