本篇博客對單例模式的餓漢式、懶漢式應用在多線程下是否存在安全隱患及其解決方法進行細節講述。面試
單例模式設計模式
定義:確保一個類只有一個實例,並且自行實例化並向整個系統提供這個實例。安全
類型: 建立類模式多線程
單例模式是一種經常使用的軟件設計模式。在它的核心結構中只包含一個被稱爲單例類的特殊類。經過單例模式能夠保證系統中一個類只有一個實例並且該實例易於外界訪併發
問,從而方便對實例個數的控制並節約系統資源。若是但願在系統中某個類的對象只能存在一個,單例模式是最好的解決方案。函數
要點:主要有三個,一是某個類只能有一個實例;二是它必須自行建立這個實例;三是它必須自行向整個系統提供這個實例。this
注意:如何保證對象的惟一性?spa
單例的步驟:線程
單例模式中的餓漢式(又叫單例設計模式)、懶漢式(又叫延遲加載設計模式)。設計
1 //餓漢式(開發時經常使用) 2 class Single{//類一加載,對象就已經存在了 3 private static final Single S= new Single(); 4 private Single();//私有化該類的構造函數,這樣其它類就不可以再用new方式建立對象了。 5 public static Single getInstance(){ 6 return S;//返回一個本類對象,由於該方法是靜態的,靜態的方法訪問的只能是靜態的S,因此Single S = new Single()必須是靜態的。 7 } 8 } 9 10 class SingleDemo{ 11 public static void main(String[] args){ 12 Single ss = Single.getInstance();//由於Single類的構造函數已經private(私有化了),因此此處只能用 類名.函數名 調用getInstance方法。 13 //可是類名調用有前提:必須是靜態的方法.因此getInstance是靜態方法,要有static修飾, 14 //而該方法中返回一個已經建立好的對象ss,保證只有一個對象. 15 } 16 }
1 //懶漢式(延遲加載形式)(面試常被問到) 2 class Single{//類一加載,只是先建立引用,只有調用了getInstance方法,纔會建立對象 3 private static Single S = null; 4 private Single(); 5 public static Single getInstance(){ 6 if(S==null){ 7 S = new Single(); 8 return S; 9 } 10 } 11 } 12 //若是後期被多線程併發訪問時,保證不了對象的惟一性,存在安全隱患,若是改後,效率下降 13 class SingleDemo{ 14 public static void main(String[] args){ 15 Single ss = Single.getInstance(); 16 } 17 }
問題1. 分析二者在多線程併發訪問狀況下存在的安全隱患?
說到這裏討論多線程併發訪問下存在的安全隱患,就必需要了解一下,線程安全問題產生的緣由是什麼?
當一個線程在執行操做共享數據的多行代碼過程當中,其它線程參與了運算就會致使線程安全問題的產生。
解決思路:就是將多條操做共享數據的代碼封裝起來,當有線程在執行這些代碼的時候,其餘線程不可參與運算。必需要當前線程把這些代碼都執行完畢後,其餘線程不可參與運算。
(1)在Java中,用同步代碼塊就能夠解決這個問題
同步代碼塊格式: synchronized(對象){
須要被同步的代碼塊
}
同步的前提:同步中必須有多個線程並使用同一把鎖
同步的好處:解決了線程的安全問題
同步的弊端:會相應下降效率,由於線程外的其它線程都會判斷同步鎖。
(2)同步函數也能夠解決問題:將synchronized放到方法中修飾
(3)同步函數與同步代碼塊的區別?
同步函數使用的鎖是固定的this。同步代碼塊使用的鎖是任意的對象(建議使用)
(4)靜態的同步函數使用的瑣是:該函數所屬字節碼文件對象,可用當前 類名.class 表示。
對於安全隱患分析:
在餓漢式中(不存在安全隱患),當遇到多線程併發訪問時,getInstance加載到線程的run()方法中,返回的對象S是共享數據,由於操做共享數據的代碼只有一句,因此不會涉及到線程安全問題。
在懶漢式中(存在安全隱患),共享數據是S,且操做共享數據的代碼有多條,當其中一個線程A在執行期間,執行到S=new Single();前(即執行完if(S==null),但尚未執行S=new Single()時 ),忽然被切換執行權,致使下一條線程B開始執行,一直到線程B建立完對象S後,線程A從新得到執行權,此時線程A又會再次執行S=new Single();再次建立對象,這樣線程A、線程B各自建立了一個對象,致使對象不惟一,也就不能是單例了!
2. 存在的安全隱患如何解決?
(1)首先解決懶漢式中多線程併發訪問時存在的對象不惟一的安全隱患!(即在getInstance()方法上用synchronized修飾)
1 //懶漢式(延遲加載形式)(面試常被問到) 2 class Single{//類一加載,只是先建立引用,只有調用了getInstance方法,纔會建立對象 3 private static Single S = null; 4 private Single(); 5 public static synchronized Single getInstance(){ 6 if(S==null){ 7 S = new Single(); 8 return S; 9 } 10 } 11 } 12 13 class SingleDemo{ 14 public static void main(String[] args){ 15 Single ss = Single.getInstance(); 16 } 17 }
(2)問題2. 問題又來了,由於使用同步函數,雖然解決了安全隱患,可是一樣的下降了效率。那麼是什麼緣由致使多線程併發訪問的效率下降了呢?
分析:由於第一次建立完對象後,以後的線程每一次獲取對象都要先判斷同步鎖,所以致使效率下降。
問題3. 懶漢式在線程安全前提下如何提升效率?
解決思路:不用同步函數中的同步,改用同步代碼塊中的同步,上代碼
1 //懶漢式(延遲加載形式)(面試常被問到) 2 class Single{//類一加載,只是先建立引用,只有調用了getInstance方法,纔會建立對象 3 private static Single S = null; 4 private Single(); 5 public static Single getInstance(){ 6 if(S==null){//此處多加了一次判斷 7 synchronized(Single.class){//使用同步代碼塊,此處注意使用Single.class鎖,由於getInstance()是靜態方法,因此只能用Single.class .而this.getClass()鎖是用於非靜態的方法。 8 if(S==null){ 9 S = new Single(); 10 } 11 } 12 return S; 13 } 14 } 15 } 16 17 class SingleDemo{ 18 public static void main(String[] args){ 19 Single ss = Single.getInstance(); 20 } 21 }
上述代碼中,多加了一次判斷是爲了解決效率問題,這樣當線程A建立完對象後,線程B在運行時首先判斷S==null,此時由於線程A已經建立完對象,因此線程B就不在執行建立對象操做。使用同步鎖是爲了解決安全問題。(以上都是以程序中只有線程A、線程B來解釋的,再多線程原理也是同樣的)
結論:因此說真正程序開發時,多數用的是餓漢式;懶漢式當多線程併發訪問時,保證不了對象的惟一性(多線程切換執行權執行程序,致使對象不惟一),雖通過修改保證安全,可是其效率相應下降。