淺談java 單例模式DCL的缺陷及單例的正確寫法
1 前言
單例模式是我們經(jīng)常使用的一種模式,一般來說很多資料都建議我們寫成如下的模式:
/**
* Created by qiyei2015 on 2017/5/13.
*/
public class Instance {
private String str = "";
private int a = 0;
private static Instance ins = null;
/**
* 構(gòu)造方法私有化
*/
private Instance(){
str = "hello";
a = 20;
}
/**
* DCL方式獲取單例
* @return
*/
public static Instance getInstance(){
if (ins == null){
synchronized (Instance.class){
if (ins == null){
ins = new Instance();
}
}
}
return ins;
}
}
但是這種方式其實(shí)是有缺陷的,具體什么缺陷呢?我們首先要了解JVM了內(nèi)存模型,請(qǐng)看下面分析
2 JVM內(nèi)存模型
JVM模型如下圖:

這里著重介紹下VM Stack,其他的我相信都比較熟悉。
VM Stack是線程私有的區(qū)域。他是java方法執(zhí)行時(shí)的字典:它里面記錄了局部變量表、 操作數(shù)棧、 動(dòng)態(tài)鏈接、 方法出口等信息。
在《java虛擬機(jī)規(guī)范》一書中對(duì)這部分的描述如下:
棧幀( Frame)是用來存儲(chǔ)數(shù)據(jù)和部分過程結(jié)果的數(shù)據(jù)結(jié)構(gòu),同時(shí)也被用來處理動(dòng)態(tài)鏈接 (Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。
棧幀隨著方法調(diào)用而創(chuàng)建,隨著方法結(jié)束而銷毀——無論方法是正常完成還是異常完成(拋出了在方法內(nèi)未被捕獲的異常)都算作方法結(jié)束。
棧幀的存儲(chǔ)空間分配在 Java 虛擬機(jī)棧( §2.5.5)之中,每一個(gè)棧幀都有自己的局部變量表( Local Variables, §2.6.1)、操作數(shù)棧( OperandStack, §2.6.2)和指向當(dāng)前方法所屬的類的運(yùn)行時(shí)常量池( §2.5.5)的引用。
java中某個(gè)線程在訪問堆中的線程共享變量時(shí),為了加快訪問速度,提升效率,會(huì)把該變量臨時(shí)拷貝一份到自己的VM Stack中,并保持和堆中數(shù)據(jù)的同步。
3 傳統(tǒng)DCL方式的缺陷
有了以上的基礎(chǔ)知識(shí)我們就可以知道DCL方式的缺陷在哪兒了。當(dāng)線程A在獲取了Instance.class鎖時(shí),對(duì)ins進(jìn)行 ins = new Instance() 初始化時(shí),由于這是很多條指令,jvm可能會(huì)亂序執(zhí)行。
這個(gè)時(shí)候如果線程B在執(zhí)行if (ins == null)時(shí),正常情況下,如果為true,說明需要獲取Instance.class鎖,等待初始化。
但是這時(shí)候,假設(shè)線程A再?zèng)]有對(duì)ins進(jìn)行初始化完,比如只對(duì)str進(jìn)行了賦值,還沒有來的及對(duì)a進(jìn)行賦值,假如jvm將未完成賦值的值拷貝回堆中,這個(gè)時(shí)候線程B有可能讀到的值就不是為null了,就會(huì)造成數(shù)據(jù)丟失的情況。這時(shí)候我們發(fā)現(xiàn)線程B獲取的對(duì)象中a的值是0,而不是20
因?yàn)椋簩?duì)ins的寫操作不 happen-before 對(duì)它的讀操作
這就是DCL方式的缺陷,那么怎么避免呢?首先我們需要了解分析多線程的一大利器
4 happen-before原則
Happen-Before規(guī)則:
1 同一個(gè)線程中,書寫在前面的操作happen-before書寫在后面的操作。這條規(guī)則是說,在單線程 中操作間happen-before關(guān)系完全是由源代碼的順序決定的,這里的前提“在同一個(gè)線程中”是很重要的,這條規(guī)則也稱為單線程規(guī)則 。
這個(gè)規(guī)則多少說得有些簡(jiǎn)單了,考慮到控制結(jié)構(gòu)和循環(huán)結(jié)構(gòu),書寫在后面的操作可能happen-before書寫在前面的操作,不過我想讀者應(yīng)該明白我的意思。
2 對(duì)鎖的unlock操作happen-before后續(xù)的對(duì)同一個(gè)鎖的lock操作。這里的“后續(xù)”指的是時(shí)間上的先后關(guān)系,unlock操作發(fā)生在退出同步塊之后,lock操作發(fā)生在進(jìn)入同步塊之前。這是條最關(guān)鍵性的規(guī)則,線程安全性主要依賴于這條規(guī)則。
但是僅僅是這條規(guī)則仍然不起任何作用,它必須和下面這條規(guī)則聯(lián)合起來使用才顯得意義重大。這里關(guān)鍵條件是必須對(duì)“同一個(gè)鎖”的lock和unlock。
如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。這條規(guī)則也稱為傳遞規(guī)
3 對(duì)volatile字段的寫操作happen-before后續(xù)的對(duì)同一個(gè)字段的讀操作.(Java5 新增)
4 單例模式的正確寫法
有了以上的分析我們知道,我們只需要在保證對(duì)ins的訪問是讀在寫之后即可,因此正確的做法是在ins 前加上一個(gè)關(guān)鍵字volatile。因此DCL的正確寫法應(yīng)該如下:
/**
* Created by qiyei2015 on 2017/5/13.
*/
public class Instance {
private String str = "";
private int a = 0;
private volatile static Instance ins = null;
/**
* 構(gòu)造方法私有化
*/
private Instance(){
str = "hello";
a = 20;
}
/**
* DCL方式獲取單例
* @return
*/
public static Instance getInstance(){
if (ins == null){
synchronized (Instance.class){
if (ins == null){
ins = new Instance();
}
}
}
return ins;
}
}
其實(shí)單例模式也有另一種我很喜歡的寫法,那就是內(nèi)部類:
/**
* Created by qiyei2015 on 2017/5/13.
*/
public class Instance {
/**
* 構(gòu)造方法私有化
*/
private Instance(){
}
private static class SingleHolder{
private static final Instance ins = new Instance();
}
/**
* 內(nèi)部類方式獲取單例
* @return
*/
public static Instance getInstance(){
return SingleHolder.ins;
}
}
這種從jvm虛擬機(jī)上保證了單例,并且也是懶式加載。
以上這篇淺談java 單例模式DCL的缺陷及單例的正確寫法就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java如何主動(dòng)從當(dāng)前線程獲取異常信息
這篇文章主要介紹了Java如何主動(dòng)從當(dāng)前線程獲取異常信息,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10
SpringCloud負(fù)載均衡實(shí)現(xiàn)定向路由詳情
這篇文章主要介紹了SpringCloud負(fù)載均衡實(shí)現(xiàn)定向路由詳情,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08
身份證號(hào)碼驗(yàn)證算法深入研究和Java實(shí)現(xiàn)
這篇文章主要介紹了身份證號(hào)碼驗(yàn)證算法深入研究和Java實(shí)現(xiàn),本文講解了18身份證號(hào)碼的結(jié)構(gòu)、根據(jù)17位數(shù)字本體碼獲取最后一位校驗(yàn)碼程序?qū)嵗葍?nèi)容,需要的朋友可以參考下2015-06-06
Java中實(shí)體與Map之間的相互轉(zhuǎn)換代碼示例
生活中經(jīng)常用到map數(shù)據(jù)與實(shí)體類的轉(zhuǎn)換,下面這篇文章主要給大家介紹了關(guān)于Java中實(shí)體與Map之間相互轉(zhuǎn)換的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12
Spring Hibernate實(shí)現(xiàn)分頁(yè)功能
這篇文章主要為大家詳細(xì)介紹了Spring Hibernate實(shí)現(xiàn)分頁(yè)功能的相關(guān)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
AsyncHttpClient KeepAliveStrategy源碼流程解讀
這篇文章主要為大家介紹了AsyncHttpClient KeepAliveStrategy源碼流程解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12

