首頁  >  文章  >  Java  >  Spring下單例模式與線程安全之間的矛盾解決

Spring下單例模式與線程安全之間的矛盾解決

不言
不言轉載
2018-10-22 17:30:084426瀏覽

這篇文章帶給大家的內容是關於Spring下單例模式與線程安全之間的矛盾解決,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

有多少人在使用Spring框架時,很多時候不知道或忽略了多執行緒的問題?

因為寫程式時,或做單元測試時,很難有機會碰到多執行緒的問題,因為沒有那麼容易模擬多執行緒測試的環境。那麼當多個執行緒呼叫同一個bean的時候就會存在線程安全問題。如果是Spring中bean的創建模式為非單例的,也就不存在這樣的問題了。

但如果不去考慮潛在的漏洞,它就會變成程式的隱形殺手,在你不知道的時候爆發。而且,通常是程式交付使用時,在生產環境下觸發,會是很麻煩的事。

Spring使用ThreadLocal解決線程安全問題

我們知道在一般情況下,只有無狀態的Bean可以在多執行緒環境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對某些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態採用ThreadLocal進行處理,讓它們也成為線程安全的狀態,因為有狀態的Bean就可以在多線程中共享了。

一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層透過介面向上層開放功能呼叫。在一般情況下,從接收請求到回傳回應所經過的所有程式呼叫都同屬於一個執行緒。

ThreadLocal是解決執行緒安全性問題一個很好的思路,它透過為每個執行緒提供一個獨立的變數副本解決了變數並發存取的衝突問題。在許多情況下,ThreadLocal比直接使用synchronized同步機制解決執行緒安全性問題更簡單,更方便,且結果程式擁有更高的並發性。

如果你的程式碼所在的進程中有多個執行緒在同時執行,而這些執行緒可能會同時運行這段程式碼。如果每次運行結果和單執行緒運行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。或者說:一個類別或程式所提供的介面對於執行緒來說是原子操作或是多個執行緒之間的切換不會導致該介面的執行結果存在二義性,也就是說我們不用考慮同步的問題。 線程安全問題都是由全域變數及靜態變數引起的。

若每個執行緒中對全域變數、靜態變數只有讀取操作,而無寫操作,一般來說,這個全域變數是執行緒安全的;若有多個執行緒同時執行寫入操作,一般都需要考慮線程同步,否則就可能影響線程安全。
1) 常數總是執行緒安全的,因為只存在讀取操作。
2)每次呼叫方法前都會新建一個實例是執行緒安全的,因為不會存取共享的資源。
3)局部變數是線程安全的。因為每執行一個方法,都會在獨立的空間創建局部變量,它不是共享的資源。局部變數包括方法的參數變數和方法內變數。

有狀態就是有資料儲存功能。有狀態物件(Stateful Bean),就是有實例變數的物件  ,可以保存數據,是非線程安全的。在不同方法呼叫間不保留任何狀態。

無狀態就是一次操作,不能儲存資料。無狀態物件(Stateless Bean),就是沒有實例變數的物件  .不能保存數據,是不變類,是執行緒安全的。

有狀態物件:

無狀態的Bean適合用不變模式,技術就是單例模式,這樣可以共享實例,提高效能。有狀態的Bean,多執行緒環境下不安全,那麼適合用Prototype原型模式。 Prototype: 每次對bean的請求都會建立一個新的bean實例。

Struts2預設的實作是Prototype模式。也就是每個請求都新產生一個Action實例,所以不存在線程安全問題。要注意的是,如果Spring管理action的生命週期, scope要配成prototype作用域

執行緒安全案例

##SimpleDateFormat( 下面簡稱 sdf) 類別內部有一個 Calendar 對象引用 , 它用來儲存和這個 sdf 相關的日期資訊 , 例如 sdf.parse(dateStr), sdf.format(date)  Calendar 引用來儲存的 . 這樣就會導致一個問題 , 如果你的 sdf 是個 static 的 ,  那麼多 thread  與你分享這個 sdf, 你會發現有以下的呼叫 :

 Date parse() {
   calendar.clear(); // 清理calendar
   ... // 执行一些操作, 设置 calendar 的日期什么的
   calendar.getTime(); // 获取calendar的时间
 }

這裡會導致的問題就是 ,  如果 線程 A  調用了  sdf.parse(),  並且進行了 calendar.clear() 後未執行 calendar.getTime()  ). (), 這時候線程 B 也執行了 sdf.clear() 方法 ,  這樣就導致線程 A 的的 calendar 資料被清空了 ( 實際上 A,B clear()  後被掛起 ,  這時候 B  開始調用 sdf.parse() 並順利 i 結束 ,  這樣  A  的設定對的問題 背後隱藏著一個更重要的問題 -- 無狀態:無狀態方法的好處之一,就是它在各種環境下,都可以安全的呼叫。衡量一個方法是否是有狀態的,就看它是否改動了其它的東西,例如全域變量,例如實例的欄位。 format 方法在運作過程中改變了SimpleDateFormat 的 calendar 字段,所以,它是有狀態的。

這也同時提醒我們在開發和設計系統的時候注意下以下三點 :

#自己寫公用類別的時候,要對多執行緒呼叫情況下的後果在註解裡進行明確說明

對線程環境下,對每一個共享的可變變數都要注意其線程安全性

我們的類別和方法在做設計的時候,要盡量設計成無狀態的

解決方案

1. 需要的時候建立新實例:

說明:在需要用到 SimpleDateFormat的地方新建一個實例,不管什麼時候,將有線程安全問題的物件由共享變為局部私有都能避免多線程問題,不過也加重了創建物件的負擔。在一般情況下,這樣其實對效能影響比不是很明顯的。

2. 使用同步:同步 SimpleDateFormat 物件

public class DateSyncUtil {
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      
    public static String formatDate(Date date)throws ParseException{
        synchronized(sdf){
            return sdf.format(date);
        }  
    }
    
    public static Date parse(String strDate) throws ParseException{
        synchronized(sdf){
            return sdf.parse(strDate);
        }
    } 
}
說明:當執行緒較多時,當一個執行緒呼叫該方法時,其他想要呼叫此方法的執行緒就要block ,多執行緒並發量大的時候會對效能有一定的影響。

3. 使用 ThreadLocal :

public class ConcurrentDateUtil {
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }
    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}

ThreadLocal<DateFormat>(); 
 
    public static DateFormat getDateFormat()   
    {  
        DateFormat df = threadLocal.get();  
        if(df==null){  
            df = new SimpleDateFormat(date_format);  
            threadLocal.set(df);  
        }  
        return df;  
    }  
    public static String formatDate(Date date) throws ParseException {
        return getDateFormat().format(date);
    }
    public static Date parse(String strDate) throws ParseException {
        return getDateFormat().parse(strDate);
    }   
}

說明:使用 ThreadLocal,  也是將共享變數變成獨享,執行緒獨享肯定能比方法獨享在並發環境中能減少不少創建物件的開銷。如果在效能要求比較高的情況下,一般建議使用此方法。

4. 拋棄 JDK ,使用其他類別庫中的時間格式化類別:

使用 Apache commons  裡的 FastDateFormat ,並宣稱有快速且執行緒的SimpleDateFormatat ,  可惜它只能對日期進行 format,  不能對日期串進行解析。

使用 Joda-Time 類別函式庫來處理時間相關問題

做一個簡單的壓力測試,方法一最慢,方法三最快,但是就算是最慢的方法一效能也不差,一般系統方法一和方法二就可以滿足,所以說這個點很難成為你係統的瓶頸所在。從簡單的角度來說,建議使用方法一或方法二,如果在必要的時候,追求那麼一點性能提升的話,可以考慮用方法三,用 ThreadLocal 做緩存。

Joda-Time 類別庫對時間處理方式比較完美,建議使用。

總結

回到文章開頭的問題:《有多少人在使用Spring框架時,很多時候不知道或忽略了多執行緒的問題? 》

其實程式碼誰都會寫,為什麼架構師寫的程式碼效果和你的天差地別呢?應該就是這類你沒考慮到的小問題而架構師都考慮到了。

架構師知識面更廣,見識到的具體情況更多,解決各類問題的經驗更豐富。只要養成架構師的思維和習慣,那你離架構師還會遠嗎?

以上是Spring下單例模式與線程安全之間的矛盾解決的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除