本文章是在學習了 微信公衆號 「java後端技術 」 以後本身的學習筆記 。 其中直接 複製了 至關部分的原做者的原文。java
若是您看到了個人這篇文章, 推薦您 查看原文後端
原文鏈接 : https://mp.weixin.qq.com/s/CfekzTTT-a066_PyT_n_eA 設計模式
在之前本身也瞭解過一些設計模式, 這其中就包括了單例模式, 可是 對單例模式只限於 基本的 懶漢式 和 餓漢式 :安全
餓漢式代碼示例 :微信
public DemoSingle{ //私有化構造器 private DemoSingle(){} //提早構造好方法 private static DemoSingle single = new DemoSingle(); //提供暴露對象的方法 public DemoSingle getDemoSingle(){ return single; } }
懶漢式代碼示例 :多線程
/** * Create by yaoming on 2018/4/27 */ public class DemoSingle { //私有化構造方法 private DemoSingle(){} //私有化 本類對象引用 private static DemoSingle single = null; //獲得本類方法的引用 public DemoSingle getDemoSingel(){ synchronized (DemoSingle.class){ if(single == null){ single = new DemoSingle(); } } return single; } }
所謂單列模式就是說, 全局在任何一個地方發使用到的該類對象都是同一個對象,首先要保證 對象一直存在(一直有引用指向對象),因此,使用一個靜態引用併發
指向該類。 同時要保證 只有一個對象, 因此要私有化 構造方法, 使得只有本身能構造這個對象(並且本身必須構造且之構造一個該對象)。函數
餓漢式 是在加載該類的時候就進行了對象的創建,不管咱們是否使用到了 這個對象。 其安全有效, 不涉及多線程操做。 可是其形成了資源的浪費。性能
懶漢式 在實際狀況中咱們可能爲了性能着想, 每每但願能使用延遲加載的方式來建立對象, 這個就是懶漢式了。學習
上面的懶漢式代碼,爲了考慮多線程的關係, 加了一個同步代碼塊, 這樣雖然解決了 多線程安全問題, 可是卻由於每次都會進行一個同步狀況下的判斷,
每每使得效率並,並無增長, 用原文做者的話來講就是 : 使用一個 百分之百的盾 來 阻擋一個 百分之一 的出現的問題。 這顯然不合適。
遂優化 :
public class DemoSingle { //私有化構造方法 private DemoSingle(){} //私有化 本類對象引用 private static DemoSingle single = null; //獲得本類方法的引用 public DemoSingle getDemoSingel(){ if(single == null){ synchronized (DemoSingle.class){ if(single == null){ single = new DemoSingle(); } } } return single; } }
這個代碼就是原來我對於懶漢式的理解了, 在看了原做者的文章後, 才發如今這個看似完美的代碼下面隱藏的問題,
這裏 原做者 談到了兩個概念 : 原子操做 和 指令重排
這裏是做者原文 :
原子操做:
簡單來講,原子操做(atomic)就是不可分割的操做,在計算機中,就是指不會由於線程調度被打斷的操做。好比,簡單的賦值是一個原子操做:
m = 6; // 這是個原子操做 |
假如m原先的值爲0,那麼對於這個操做,要麼執行成功m變成了6,要麼是沒執行 m仍是0,而不會出現諸如m=3這種中間態——即便是在併發的線程中。
可是,聲明並賦值就不是一個原子操做:
int n=6;//這不是一個原子操做 |
對於這個語句,至少有兩個操做:①聲明一個變量n ②給n賦值爲6——這樣就會有一箇中間狀態:變量n已經被聲明瞭可是尚未被賦值的狀態。這樣,在多線程中,因爲線程執行順序的不肯定性,若是兩個線程都使用m,就可能會致使不穩定的結果出現。
指令重排:
簡單來講,就是計算機爲了提升執行效率,會作的一些優化,在不影響最終結果的狀況下,可能會對一些語句的執行順序進行調整。好比,這一段代碼:
int a ; // 語句1 |
正常來講,對於順序結構,執行的順序是自上到下,也即1234。可是,因爲指令重排
的緣由,由於不影響最終的結果,因此,實際執行的順序可能會變成3124或者1324。
因爲語句3和4沒有原子性的問題,語句3和語句4也可能會拆分紅原子操做,再重排。——也就是說,對於非原子性的操做,在不影響最終結果的狀況下,其拆分紅的原子操做可能會被從新排列執行順序。
OK,瞭解了原子操做和指令重排的概念以後,咱們再繼續看代碼三的問題。
主要在於singleton = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。
1. 給 singleton 分配內存
2. 調用 Singleton 的構造函數來初始化成員變量,造成實例
3. 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null了)
在JVM的即時編譯器中存在指令重排序的優化。
也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。
再稍微解釋一下,就是說,因爲有一個『instance已經不爲null可是仍沒有完成初始化』的中間狀態,而這個時候,若是有其餘線程恰好運行到第一層if (instance ==null)這裏,這裏讀取到的instance已經不爲null了,因此就直接把這個中間狀態的instance拿去用了,就會產生問題。這裏的關鍵在於線程T1對instance的寫操做沒有完成,線程T2就執行了讀操做。
因而可知, 個人第二段 懶漢式代碼存在 隱患 , 根據做者思路 將之改成 :
public class DemoSingle { //私有化構造方法 private DemoSingle(){} //私有化 本類對象引用 private static volatile DemoSingle single = null; //獲得本類方法的引用 public DemoSingle getDemoSingel(){ if(single == null){ synchronized (DemoSingle.class){ if(single == null){ single = new DemoSingle(); } } } return single; } }
其實就是加上了一個 volatitle 關鍵字 , 這裏 volatitle 關鍵字的做用是禁止 指令重排, 在對 single 進行復制完成以前是不會進行 讀操做的。
(做者原文 : 注意:volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操做([1-2-3])完成以前,不會調用讀操做(if (instance == null))。)
這樣就解決了傳統的 懶漢式單例模式 的多線程安全問題, 除此以外 原做者還提供了 其餘兩種更爲簡便的 方式:
public class DemoSingle { //私有化構造方法 private DemoSingle(){} //靜態內部類 private static class DemoSingleHand{ private static final DemoSingle DEMO_SINGLE = new DemoSingle(); } //得到該類對象的方法 public static DemoSingle getDemoSingel(){ return DemoSingleHand.DEMO_SINGLE; } }
這種寫法的巧妙之處在於:對於內部類SingletonHolder,它是一個餓漢式的單例實現,在SingletonHolder初始化的時候會由ClassLoader來保證同步,使INSTANCE是一個真單例。
同時,因爲SingletonHolder是一個內部類,只在外部類的Singleton的getInstance()中被使用,因此它被加載的時機也就是在getInstance()方法第一次被調用的時候。
它利用了ClassLoader來保證了同步,同時又能讓開發者控制類加載的時機。從內部看是一個餓漢式的單例,可是從外部看來,又的確是懶漢式的實現
是否是很簡單?並且由於自動序列化機制,保證了線程的絕對安全。三個詞歸納該方式:簡單、高效、安全
這種寫法在功能上與共有域方法相近,可是它更簡潔,無償地提供了序列化機制,絕對防止對此實例化,即便是在面對複雜的序列化或者反射攻擊的時候。雖然這中方法尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。
原文地址:https://gyl-coder.top/Java%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/