這篇文章主要介紹了解決Spring框架單例模式與線(xiàn)程安全之間矛盾的方法,具有一定借鑒價(jià)值,需要的朋友可以參考下。如下資料是關(guān)于解決Spring框架單例模式與線(xiàn)程安全之間矛盾的方法的詳細(xì)步驟內(nèi)容。
站在用戶(hù)的角度思考問(wèn)題,與客戶(hù)深入溝通,找到拉薩網(wǎng)站設(shè)計(jì)與拉薩網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個(gè)性化、用戶(hù)體驗(yàn)好的作品,建站類(lèi)型包括:網(wǎng)站制作、做網(wǎng)站、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、空間域名、雅安服務(wù)器托管、企業(yè)郵箱。業(yè)務(wù)覆蓋拉薩地區(qū)。因?yàn)閷?xiě)程序時(shí),或做單元測(cè)試時(shí),很難有機(jī)會(huì)碰到多線(xiàn)程的問(wèn)題,因?yàn)闆](méi)有那么容易模擬多線(xiàn)程測(cè)試的環(huán)境。那么當(dāng)多個(gè)線(xiàn)程調(diào)用同一個(gè)bean的時(shí)候就會(huì)存在線(xiàn)程安全問(wèn)題。如果是Spring中bean的創(chuàng)建模式為非單例的,也就不存在這樣的問(wèn)題了。
但如果不去考慮潛在的漏洞,它就會(huì)變成程序的隱形殺手,在你不知道的時(shí)候爆發(fā)。而且,通常是程序交付使用時(shí),在生產(chǎn)環(huán)境下觸發(fā),會(huì)是很麻煩的事。
Spring使用ThreadLocal解決線(xiàn)程安全問(wèn)題
我們知道在一般情況下,只有無(wú)狀態(tài)的Bean才可以在多線(xiàn)程環(huán)境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因?yàn)镾pring對(duì)一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線(xiàn)程安全狀態(tài)采用ThreadLocal進(jìn)行處理,讓它們也成為線(xiàn)程安全的狀態(tài),因?yàn)橛袪顟B(tài)的Bean就可以在多線(xiàn)程中共享了。
一般的Web應(yīng)用劃分為展現(xiàn)層、服務(wù)層和持久層三個(gè)層次,在不同的層中編寫(xiě)對(duì)應(yīng)的邏輯,下層通過(guò)接口向上層開(kāi)放功能調(diào)用。在一般情況下,從接收請(qǐng)求到返回響應(yīng)所經(jīng)過(guò)的所有程序調(diào)用都同屬于一個(gè)線(xiàn)程。
ThreadLocal是解決線(xiàn)程安全問(wèn)題一個(gè)很好的思路,它通過(guò)為每個(gè)線(xiàn)程提供一個(gè)獨(dú)立的變量副本解決了變量并發(fā)訪(fǎng)問(wèn)的沖突問(wèn)題。在很多情況下,ThreadLocal比直接使用synchronized同步機(jī)制解決線(xiàn)程安全問(wèn)題更簡(jiǎn)單,更方便,且結(jié)果程序擁有更高的并發(fā)性。
如果你的代碼所在的進(jìn)程中有多個(gè)線(xiàn)程在同時(shí)運(yùn)行,而這些線(xiàn)程可能會(huì)同時(shí)運(yùn)行這段代碼。如果每次運(yùn)行結(jié)果和單線(xiàn)程運(yùn)行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線(xiàn)程安全的。 或者說(shuō):一個(gè)類(lèi)或者程序所提供的接口對(duì)于線(xiàn)程來(lái)說(shuō)是原子操作或者多個(gè)線(xiàn)程之間的切換不會(huì)導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性,也就是說(shuō)我們不用考慮同步的問(wèn)題。線(xiàn)程安全問(wèn)題都是由全局變量及靜態(tài)變量引起的。
若每個(gè)線(xiàn)程中對(duì)全局變量、靜態(tài)變量只有讀操作,而無(wú)寫(xiě)操作,一般來(lái)說(shuō),這個(gè)全局變量是線(xiàn)程安全的;若有多個(gè)線(xiàn)程同時(shí)執(zhí)行寫(xiě)操作,一般都需要考慮線(xiàn)程同步,否則就可能影響線(xiàn)程安全。
1) 常量始終是線(xiàn)程安全的,因?yàn)橹淮嬖谧x操作。
2)每次調(diào)用方法前都新建一個(gè)實(shí)例是線(xiàn)程安全的,因?yàn)椴粫?huì)訪(fǎng)問(wèn)共享的資源。
3)局部變量是線(xiàn)程安全的。因?yàn)槊繄?zhí)行一個(gè)方法,都會(huì)在獨(dú)立的空間創(chuàng)建局部變量,它不是共享的資源。局部變量包括方法的參數(shù)變量和方法內(nèi)變量。
有狀態(tài)就是有數(shù)據(jù)存儲(chǔ)功能。有狀態(tài)對(duì)象(Stateful Bean),就是有實(shí)例變量的對(duì)象 ,可以保存數(shù)據(jù),是非線(xiàn)程安全的。在不同方法調(diào)用間不保留任何狀態(tài)。
無(wú)狀態(tài)就是一次操作,不能保存數(shù)據(jù)。無(wú)狀態(tài)對(duì)象(Stateless Bean),就是沒(méi)有實(shí)例變量的對(duì)象 .不能保存數(shù)據(jù),是不變類(lèi),是線(xiàn)程安全的。
有狀態(tài)對(duì)象:
無(wú)狀態(tài)的Bean適合用不變模式,技術(shù)就是單例模式,這樣可以共享實(shí)例,提高性能。有狀態(tài)的Bean,多線(xiàn)程環(huán)境下不安全,那么適合用Prototype原型模式。Prototype: 每次對(duì)bean的請(qǐng)求都會(huì)創(chuàng)建一個(gè)新的bean實(shí)例。
Struts2默認(rèn)的實(shí)現(xiàn)是Prototype模式。也就是每個(gè)請(qǐng)求都新生成一個(gè)Action實(shí)例,所以不存在線(xiàn)程安全問(wèn)題。需要注意的是,如果由Spring管理action的生命周期, scope要配成prototype作用域
線(xiàn)程安全案例
SimpleDateFormat( 下面簡(jiǎn)稱(chēng) sdf) 類(lèi)內(nèi)部有一個(gè) Calendar 對(duì)象引用 , 它用來(lái)儲(chǔ)存和這個(gè) sdf 相關(guān)的日期信息 , 例如 sdf.parse(dateStr), sdf.format(date) 諸如此類(lèi)的方法參數(shù)傳入的日期相關(guān) String, Date 等等 , 都是交友 Calendar 引用來(lái)儲(chǔ)存的 . 這樣就會(huì)導(dǎo)致一個(gè)問(wèn)題 , 如果你的 sdf 是個(gè) static 的 , 那么多個(gè) thread 之間就會(huì)共享這個(gè) sdf, 同時(shí)也是共享這個(gè) Calendar 引用 , 并且 , 觀察 sdf.parse() 方法 , 你會(huì)發(fā)現(xiàn)有如下的調(diào)用 :
Date parse() { calendar.clear(); // 清理calendar ... // 執(zhí)行一些操作, 設(shè)置 calendar 的日期什么的 calendar.getTime(); // 獲取calendar的時(shí)間 }
這里會(huì)導(dǎo)致的問(wèn)題就是 , 如果 線(xiàn)程 A 調(diào)用了 sdf.parse(), 并且進(jìn)行了 calendar.clear() 后還未執(zhí)行 calendar.getTime() 的時(shí)候 , 線(xiàn)程 B 又調(diào)用了 sdf.parse(), 這時(shí)候線(xiàn)程 B 也執(zhí)行了 sdf.clear() 方法 , 這樣就導(dǎo)致線(xiàn)程 A 的的 calendar 數(shù)據(jù)被清空了 ( 實(shí)際上 A,B 的同時(shí)被清空了 ). 又或者當(dāng) A 執(zhí)行了 calendar.clear() 后被掛起 , 這時(shí)候 B 開(kāi)始調(diào)用 sdf.parse() 并順利 i 結(jié)束 , 這樣 A 的 calendar 內(nèi)存儲(chǔ)的的 date 變成了后來(lái) B 設(shè)置的 calendar 的 date
這個(gè)問(wèn)題背后隱藏著一個(gè)更為重要的問(wèn)題 -- 無(wú)狀態(tài):無(wú)狀態(tài)方法的好處之一,就是它在各種環(huán)境下,都可以安全的調(diào)用。衡量一個(gè)方法是否是有狀態(tài)的,就看它是否改動(dòng)了其它的東西,比如全局變量,比如實(shí)例的字段。 format 方法在運(yùn)行過(guò)程中改動(dòng)了SimpleDateFormat 的 calendar 字段,所以,它是有狀態(tài)的。
這也同時(shí)提醒我們?cè)陂_(kāi)發(fā)和設(shè)計(jì)系統(tǒng)的時(shí)候注意下以下三點(diǎn) :
自己寫(xiě)公用類(lèi)的時(shí)候,要對(duì)多線(xiàn)程調(diào)用情況下的后果在注釋里進(jìn)行明確說(shuō)明
對(duì)線(xiàn)程環(huán)境下,對(duì)每一個(gè)共享的可變變量都要注意其線(xiàn)程安全性
我們的類(lèi)和方法在做設(shè)計(jì)的時(shí)候,要盡量設(shè)計(jì)成無(wú)狀態(tài)的
解決辦法
1. 需要的時(shí)候創(chuàng)建新實(shí)例:
說(shuō)明:在需要用到 SimpleDateFormat 的地方新建一個(gè)實(shí)例,不管什么時(shí)候,將有線(xiàn)程安全問(wèn)題的對(duì)象由共享變?yōu)榫植克接卸寄鼙苊舛嗑€(xiàn)程問(wèn)題,不過(guò)也加重了創(chuàng)建對(duì)象的負(fù)擔(dān)。在一般情況下,這樣其實(shí)對(duì)性能影響比不是很明顯的。
2. 使用同步:同步 SimpleDateFormat 對(duì)象
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); } } }
說(shuō)明:當(dāng)線(xiàn)程較多時(shí),當(dāng)一個(gè)線(xiàn)程調(diào)用該方法時(shí),其他想要調(diào)用此方法的線(xiàn)程就要block ,多線(xiàn)程并發(fā)量大的時(shí)候會(huì)對(duì)性能有一定的影響。
3. 使用 ThreadLocal :
public class ConcurrentDateUtil { private static ThreadLocalthreadLocal = new ThreadLocal () { @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(); 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); } }
說(shuō)明:使用 ThreadLocal, 也是將共享變量變?yōu)楠?dú)享,線(xiàn)程獨(dú)享肯定能比方法獨(dú)享在并發(fā)環(huán)境中能減少不少創(chuàng)建對(duì)象的開(kāi)銷(xiāo)。如果對(duì)性能要求比較高的情況下,一般推薦使用這種方法。
4. 拋棄 JDK ,使用其他類(lèi)庫(kù)中的時(shí)間格式化類(lèi):
使用 Apache commons 里的 FastDateFormat ,宣稱(chēng)是既快又線(xiàn)程安全的SimpleDateFormat, 可惜它只能對(duì)日期進(jìn)行 format, 不能對(duì)日期串進(jìn)行解析。
使用 Joda-Time 類(lèi)庫(kù)來(lái)處理時(shí)間相關(guān)問(wèn)題
做一個(gè)簡(jiǎn)單的壓力測(cè)試,方法一最慢,方法三最快,但是就算是最慢的方法一性能也不差,一般系統(tǒng)方法一和方法二就可以滿(mǎn)足,所以說(shuō)在這個(gè)點(diǎn)很難成為你系統(tǒng)的瓶頸所在。從簡(jiǎn)單的角度來(lái)說(shuō),建議使用方法一或者方法二,如果在必要的時(shí)候,追求那么一點(diǎn)性能提升的話(huà),可以考慮用方法三,用 ThreadLocal 做緩存。
Joda-Time 類(lèi)庫(kù)對(duì)時(shí)間處理方式比較完美,建議使用。
以上就是Spring框架單例模式與線(xiàn)程安全之間矛盾的解決方法的具體介紹,內(nèi)容較為全面,而且我也相信有相當(dāng)?shù)囊恍┕ぞ呖赡苁俏覀內(nèi)粘9ぷ骺赡軙?huì)見(jiàn)到或用到的。通過(guò)這篇文章,希望你能收獲更多。