淺談java 單例模式DCL的缺陷及單例的正確寫法
1 前言
單例模式是我們經(jīng)常使用的一種模式,一般來(lái)說(shuō)很多資料都建議我們寫成如下的模式:
/** * 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)是用來(lái)存儲(chǔ)數(shù)據(jù)和部分過(guò)程結(jié)果的數(shù)據(jù)結(jié)構(gòu),同時(shí)也被用來(lái)處理動(dòng)態(tài)鏈接 (Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。
棧幀隨著方法調(diào)用而創(chuàng)建,隨著方法結(jié)束而銷毀——無(wú)論方法是正常完成還是異常完成(拋出了在方法內(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,說(shuō)明需要獲取Instance.class鎖,等待初始化。
但是這時(shí)候,假設(shè)線程A再?zèng)]有對(duì)ins進(jìn)行初始化完,比如只對(duì)str進(jìn)行了賦值,還沒有來(lái)的及對(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ī)則是說(shuō),在單線程 中操作間happen-before關(guān)系完全是由源代碼的順序決定的,這里的前提“在同一個(gè)線程中”是很重要的,這條規(guī)則也稱為單線程規(guī)則 。
這個(gè)規(guī)則多少說(shuō)得有些簡(jiǎn)單了,考慮到控制結(jié)構(gòu)和循環(huán)結(jié)構(gòu),書寫在后面的操作可能happen-before書寫在前面的操作,不過(guò)我想讀者應(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)合起來(lái)使用才顯得意義重大。這里關(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è)參考,也希望大家多多支持好吧啦網(wǎng)。
相關(guān)文章:
1. ASP.NET 2.0頁(yè)面框架的幾處變化2. ASP腳本組件實(shí)現(xiàn)服務(wù)器重啟3. 用PHP建立微型論壇的簡(jiǎn)單教程4. Python爬取酷狗MP3音頻的步驟5. asp知識(shí)整理筆記4(問答模式)6. Xml簡(jiǎn)介_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理7. python 爬取豆瓣網(wǎng)頁(yè)的示例8. 在IDEA里gradle配置和使用的方法步驟9. ASP 連接Access數(shù)據(jù)庫(kù)的登陸系統(tǒng)10. 詳解JSP 內(nèi)置對(duì)象request常見用法
