摘要:好,看看大家喜聞樂見的并發場景下,這種簡易的寫法會出現什么問題兩個線程和同時訪問,它們都覺得判斷成立,分別執行了步驟,成功創建出對象但是,我們通篇都在聊單例啊,和的玩法無疑很不單例問題分析出來了,而解決上并不復雜讓線程同步就好。
單例的用處
如果你看過設計模式,肯定會知道單例模式,實際上這是我能默寫出代碼的第一個設計模式,雖然很長一段時間我并不清楚單例具體是做什么用的。
這里簡單提一下單例的用處。作為java程序員,你應該知道spring框架,而其中最核心的IOC,在默認情況下注入的Bean就是單例的。有什么好處?那些Service、Dao等只創建一次,不必每次都通過new方式創建,也就不用每次都開辟空間、垃圾回收等等,會省不少資源。
那么如何寫一個單例呢?我想很多朋友都能搞定:
public class Singleton { private static final Singleton singletonInstance = new Singleton(); // A - 急不可待的成員變量賦值,static和final修飾 private Singleton (){} // B - 私有化的構造器,避免隨意new public static Singleton getInstance(){ // C - 暴露給外部的獲取方法 return singletonInstance; } }
Ok,擁有A、B、C三大特點(注釋部分),就構成了著名的餓漢式單例。好處在于簡單粗暴,易于理解(只要你真正通曉final和static的作用)。
但有豪放派,就有婉約派。后來大家都覺得,我還沒有使用這個類,你就直接把對象構建出來扔java堆里了,是不是有點不那么含蓄?
于是大家快速迭代出懶漢式單例。
version 2: 懶漢式class Singleton { private static Singleton singletonInstance; // A - 溫婉到只有變量聲明 private Singleton (){} // B public static Singleton getInstance(){ // C if(singletonInstance==null){ singletonInstance = new Singleton(); // D - 成員變量的創建賦值延后至此 } return singletonInstance; } }
變化發生于A、D兩步,總得來說,就是把成員變量singletonInstance的創建和賦值延后了。基本的要求達到了,在沒調用getInstance()方法之前,對象無創建,不再麻煩java堆大大。一切看起來都很美好,但僅限于單線程情況下。
好,看看大家喜聞樂見的并發場景下,這種簡易的寫法會出現什么問題——兩個線程T-1和T-2同時訪問getInstance(),它們都覺得singletonInstance==null判斷成立,分別執行了步驟D,成功創建出singletonInstance對象!但是,我們通篇都在聊單例啊,T-1和T-2的玩法無疑很不單例!
問題分析出來了,而解決上并不復雜——讓線程同步就好。
class Singleton { private static Singleton singletonInstance; // A private Singleton (){} // B public static synchronized Singleton getInstance(){ // C - 用synchronized關鍵字修飾 if(singletonInstance==null){ singletonInstance = new Singleton(); // D } return singletonInstance; } }
唯一的變化在于步驟C,加入了synchronized關鍵字,讓線程同步執行此方法。現在問題解決了,不管線程T-1還是T-2,在getInstance()面前都要小朋友們排排坐——一個個執行,這樣即使是線程T-100甚至T-500過來也要排隊執行,哈哈哈哈哈哈……嗚嗚嗚……
既是解決方案,也是問題所在,這種方式效率太差了!
我們知道,synchronized有另一種使用方式就是鎖代碼塊,可以減少鎖粒度。
class Singleton { private static Singleton singletonInstance; // A private Singleton (){} // B public static Singleton getInstance(){ synchronized (Singleton.class){ // C - 改成synchronized鎖代碼塊 if(singletonInstance==null){ singletonInstance = new Singleton(); } } return singletonInstance; } }
但在這個例子中,該方式看上去似乎沒什么提升(該方法主要邏輯只有singletonInstance = new Singleton()一行)。好在有聰明人,研究出了Double-check。
version 2.2: Double-check (有問題版)class Singleton { private static Singleton singletonInstance; // A private Singleton (){} // B public static Singleton getInstance(){ if(singletonInstance==null){ // C1 - synchronized之前,第一次判斷 synchronized (Singleton.class){ if(singletonInstance==null){ // C2 - synchronized之后,第二次判斷 singletonInstance = new Singleton(); } } } return singletonInstance; } }
我一直覺得這種方式很巧妙。C1的判斷用于非并發環境,阻攔對象創建后的大部分訪問;C2的判斷,解決首次創建對象時的并發問題。
很長一段時間,我覺得這就是最終方案了,世界再次變得美好,沒想到還是圖樣圖森破(too young, too simple!)。其實不止是單例,jdk1.5之前很多問題都被一個關鍵字耽擱了——volatile,而它相關的問題深深隱藏在Java內存模型層面,且聽我緩緩道來……
算了,照顧下沒耐性的開發兄弟,先給出修改方案:
class Singleton { private static volatile Singleton singletonInstance; // A - 用volatile修飾 private Singleton (){} // B public static Singleton getInstance(){ if(singletonInstance==null){ // C1 synchronized (Singleton.class){ if(singletonInstance==null){ // C2 singletonInstance = new Singleton(); } } } return singletonInstance; } }
可以看到,唯一的變化在于A位置加入了volatile關鍵字,用于解決有序性問題。(volatile涉及的原子性和可見性這里不作討論)
有序性什么是有序性?舉個“栗子”:
int x=2;//語句1 int y=0;//語句2 boolean flag=true;//語句3 x=4;//語句4 y=-1;//語句5
對于上面的代碼來說,書寫語句按順序1至5,但執行上很可能不是這樣。有可能是1-4-3-2-5,或者1-3-2-5-4,其實只要保證1在4前并且2在5前,剩下的順序可以隨意變化。這要感謝內存模型同志,它天然允許編譯器和處理器對指令進行重排序。動機是好的——可以默默的幫你做些優化,但在并發場景下,就有好心辦壞事的嫌疑。
看下另一個例子:
Context context = null; boolean inited = false; //線程-1: public void methodA(){ context=loadContext(); //語句1 inited=true; //語句2 } //線程-2: public void methodB(){ while(!inited){ sleep(1) //語句3 } doSomethingwithconfig(context); //語句4 }
并發場景下,很可能出現如下情況:
線程-2在語句3位置無憂無慮的休眠
語句2和語句1發生指令重排,線程-1進入methodA()時先執行了語句2
恰逢線程-2覺醒,執行語句4,此時context還是null(語句1的context初始化還沒執行),災難產生
而volatile,是個“擋板”,能保證執行順序。為什么稱之為“擋板”?還以之前的“栗子”說明:
int x=2;//語句1 int y=0;//語句2 volatile boolean flag=true; //語句3 - 用volatile修飾 x=4;//語句4 y=-1;//語句5
在語句3的 boolean變量 用volatile修飾后,重排只能分別發生在1、2之間或語句4、5之間。即語句1、2不能跨過語句3,語句4、5也不能跨過語句3。
我們還需知道,對于java的某些操作,比如++,雖然看上去是一行代碼,但實質上這個操作本身并不是原子的。以i++為例,該操作實際包含i的當前值獲取,i+1計算,以及i=的賦值操作三兄弟。
同樣的,singletonInstance = new Singleton()也非原子指令,包含:
對象內存分配
初始化LazySingleton對象屬性
將singleton引用指向內存空間
如果不用volatile修飾,萬惡的指令重排可能發生在步驟2和步驟3之間,產生如下狀況(此處有盜圖嫌疑,罪過):
以上圖的情況,線程B獲取到了尚未初始化完全的LazySingleton對象,使得在后續的使用中出現異常! 用volatile修飾singleton變量后,指令重排技能被禁用,singletonInstance = new Singleton()只能按步驟1、2、3順序執行,問題就此解決。
值得一提的是,其實存在更好的volatile修飾版本。
version 2.4:推薦的volatile + Double-check 版class Singleton { private static volatile Singleton singletonInstance; // A private Singleton (){} // B public static Singleton getInstance(){ tempInstance = singletonInstance; // C - 開啟了臨時變量 if(tempInstance==null){ synchronized (Singleton.class){ if(tempInstance==null){ singletonInstance = tempInstance = new Singleton(); } } } return tempInstance ; } }
這種寫法差別在于在代碼C位置,聲明了變量tempInstance臨時變量,之后的邏輯都使用tempInstance代替singletonInstance。為什么要這樣做?wiki上準原文是這么說的:
Note the local variable "tempInstance ", which seems unnecessary. The effect of this is that in cases where singletonInstance is already initialized (i.e., most of the time), the volatile field is only accessed once (due to "return tempInstance ;" instead of "return singletonInstance;"), which can improve the method"s overall performance by as much as 25 percent.
翻譯一下就是:
singletonInstance對象大部分時候是已完成初始化的,用tempInstance臨時變量之后能減少volatile屬性(singletonInstance)的訪問,這么做大概能提升25%的性能!
哇,一不小心寫了這么多,而且還沒結束,留待下一篇吧。(主要是volatile部分比較羅嗦了,這個關鍵字各位需好好看下,借以窺探內存模型,原子性和可見性沒做分析都已經占了這么大的篇幅)
下一篇文章會包含靜態內部類實現單例、final+泛型實現單例、java9 VarHandler單例等,敬請期待!(會有人期待嗎 ::>_<:: )
https://en.wikipedia.org/wiki...
https://www.cs.umd.edu/~pugh/...
https://www.jianshu.com/p/cf5...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77849.html
摘要:如此便可使得這一實現方式能夠同時具備線程安全延遲加載以及節省大量同步判斷資源等優勢,可以說是單例模式的最佳實現了 單例模式(Singleton)是一種使用率非常高的設計模式,其主要目的在于保證某一類在運行期間僅被創建一個實例,并為該實例提供了一個全局訪問方法,通常命名為getInstance()方法。單例模式的本質簡言之即是: 控制實例數目 以Java為例,單例模式通常可分為餓漢式和懶...
摘要:分析原因實例后的不能刷新頁面,應該是因為它與全局的中的的不是同一個,而之前的版本中能直接這樣使用,應該是使用了單例。 在 github 的 vue-router 中找到同樣的一個問題:3.0.1版本通過router實例無法跳轉 昨天發現有些路由不能正常跳轉,找了一下發現都是那些實例化后使用 router.push 而不是直接使用 this.$router.push 的地方。出現的情況是...
摘要:在面向對象的語言中,比如,等,單例模式通常是定義類時將構造函數設為,保證對象不能在外部被出來,同時給類定義一個靜態的方法,用來獲取或者創建這個唯一的實例。 萬事開頭難,作為正經歷菜鳥賽季的前端player,已經忘記第一次告訴自己要寫一些東西出來是多久以的事情了。。。如果,你也和我一樣,那就像我一樣,從現在開始,從看到這篇文章開始,打開電腦,敲下你的第一篇文章(或者任何形式的文字)吧。 ...
摘要:工廠模式單例模式結構型設計模式關注于如何將類或對象組合成更大更復雜的結構,以簡化設計。 一、寫在前面 設計模式的定義:在面向對象軟件設計過程中針對特定問題的簡潔而優雅的解決方案 當然我們可以用一個通俗的說法:設計模式是解決某個特定場景下對某種問題的解決方案。因此,當我們遇到合適的場景時,我們可能會條件反射一樣自然而然想到符合這種場景的設計模式。 比如,當系統中某個接口的結構已經無法滿足...
閱讀 2440·2021-11-23 09:51
閱讀 1867·2021-10-13 09:40
閱讀 1372·2021-09-30 10:01
閱讀 589·2021-09-26 09:46
閱讀 2232·2021-09-23 11:55
閱讀 1385·2021-09-10 10:51
閱讀 2240·2021-09-09 09:33
閱讀 2227·2019-08-29 17:25