天天看點

spring 中單利模式的了解

一、Spring單例模式與線程安全

Spring架構裡的bean,或者說元件,擷取執行個體的時候都是預設的單例模式,這是在多線程開發的時候要尤其注意的地方。

單例模式的意思就是隻有一個執行個體。單例模式確定某一個類隻有一個執行個體,而且自行執行個體化并向整個系統提供這個執行個體。這個類稱為單例類。 當多使用者同時請求一個服務時,容器會給每一個請求配置設定一個線程,這是多個線程會并發執行該請求多對應的業務邏輯(成員方法),此時就要注意了,如果該處理邏輯中有對該單列狀态的修改(展現為該單列的成員屬性),則必須考慮線程同步問題 同步機制的比較  ThreadLocal和線程同步機制相比有什麼優勢呢?ThreadLocal和線程同步機制都是為了解決多線程中相同變量的通路沖突問題。      在同步機制中,通過對象的鎖機制保證同一時間隻有一個線程通路變量。這時該變量是多個線程共享的,使用同步機制要求程式慎密地分析什麼時候對變量進行讀寫,什麼時候需要鎖定某個對象,什麼時候釋放對象鎖等繁雜的問題,程式設計和編寫難度相對較大。      而ThreadLocal則從另一個角度來解決多線程的并發通路。ThreadLocal會為每一個線程提供一個獨立的變量副本,進而隔離了多個線程對資料的通路沖突。因為每一個線程都擁有自己的變量副本,進而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。      由于ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()傳回的是Object對象,需要強制類型轉換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用  概括起來說,對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊通路,而後者為每一個線程都提供了一份變量,是以可以同時通路而互不影響。      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作用域。

注解:

spring Bean生命周期

1.Bean的作用域可以通過Bean标簽的scope屬性進行設定,Bean的作用域包括:

預設情況下scope="singleton",那麼該Bean是單例,任何人擷取該Bean執行個體的都為同一個執行個體;

scope="prototype",任何一個執行個體都是新的執行個體;

scope="request",在WEB應用程式中,每一個執行個體的作用域都為request範圍;

scope="session",在WEB應用程式中,每一個執行個體的作用域都為session範圍;

注意:在預設情況下,Bean執行個體在被Spring容器初始化的時候,就會被執行個體化,預設調用無參數的構造方法。在其它情況下,Bean将會在擷取執行個體的時候才會被執行個體化。

二、線程安全案例:

SimpleDateFormat(下面簡稱sdf)類内部有一個Calendar對象引用,它用來儲存和這個sdf相關的日期資訊,例如sdf.parse(dateStr), sdf.format(date) 諸如此類的方法參數傳入的日期相關String, Date等等, 都是交友Calendar引用來儲存的.這樣就會導緻一個問題,如果你的sdf是個static的, 那麼多個thread 之間就會共享這個sdf, 同時也是共享這個Calendar引用, 并且, 觀察 sdf.parse() 方法,你會發現有如下的調用: Date parse() {     calendar.clear(); // 清理calendar     ... // 執行一些操作, 設定 calendar 的日期什麼的     calendar.getTime(); // 擷取calendar的時間   }   這裡會導緻的問題就是, 如果 線程A 調用了 sdf.parse(), 并且進行了 calendar.clear()後還未執行calendar.getTime()的時候,線程B又調用了sdf.parse(), 這時候線程B也執行了sdf.clear()方法, 這樣就導緻線程A的的calendar資料被清空了(實際上A,B的同時被清空了). 又或者當 A 執行了calendar.clear() 後被挂起, 這時候B 開始調用sdf.parse()并順利i結束, 這樣 A 的 calendar記憶體儲的的date 變成了後來B設定的calendar的date 這個問題背後隐藏着一個更為重要的問題--無狀态:無狀态方法的好處之一,就是它在各種環境下,都可以安全的調用。衡量一個方法是否是有狀态的,就看它是否改動了其它的東西,比如全局變量,比如執行個體的字段。format方法在運作過程中改動了SimpleDateFormat的calendar字段,是以,它是有狀态的。     這也同時提醒我們在開發和設計系統的時候注意下一下三點:     1.自己寫公用類的時候,要對多線程調用情況下的後果在注釋裡進行明确說明   2.對線程環境下,對每一個共享的可變變量都要注意其線程安全性   3.我們的類和方法在做設計的時候,要盡量設計成無狀态的  三.解決辦法     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);     } } 或 public class ThreadLocalDateUtil {     private static final String date_format = "yyyy-MM-dd HH:mm:ss";     private static ThreadLocal<DateFormat> threadLocal = new 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,使用其他類庫中的時間格式化類:     1.使用Apache commons 裡的FastDateFormat,宣稱是既快又線程安全的SimpleDateFormat, 可惜它隻能對日期進行format, 不能對日期串進行解析。     2.使用Joda-Time類庫來處理時間相關問題     做一個簡單的壓力測試,方法一最慢,方法三最快,但是就算是最慢的方法一性能也不差,一般系統方法一和方法二就可以滿足,是以說在這個點很難成為你系統的瓶頸所在。從簡單的角度來說,建議使用方法一或者方法二,如果在必要的時候,追求那麼一點性能提升的話,可以考慮用方法三,用ThreadLocal做緩存。

轉載于:https://www.cnblogs.com/prctice/p/5226554.html