Java中的SimpleDateFormat的線程安全問題詳解
起因
sonar 是一個代碼質(zhì)量管理工具,SonarQube是一個用于代碼質(zhì)量管理的開放平臺。
為項(xiàng)目提供可視化報告,連續(xù)追蹤項(xiàng)目質(zhì)量演化過程。
通過插件機(jī)制,Sonar可以集成不同的測試工具,代碼分析工具,以及持續(xù)集成工具。
對結(jié)果進(jìn)行再加工處理,通過量化的方式度量代碼質(zhì)量的變化。
某天它揭露了代碼里這樣一個級別為嚴(yán)重的違規(guī)。
SimpleDateFormat 源碼解析
根據(jù) sonar 的提示,SimpleDateFormat 的 format 方法可能存在線程安全的問題,那么來看下SimpleDateFormat 的源碼,看看是否如此
// Called from Format after creating a FieldDelegate private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list calendar.setTime(date); boolean useDateFormatSymbols = useDateFormatSymbols(); for (int i = 0; i < compiledPattern.length; ) { int tag = compiledPattern[i] >>> 8; int count = compiledPattern[i++] & 0xff; if (count == 255) { count = compiledPattern[i++] << 16; count |= compiledPattern[i++]; } switch (tag) { case TAG_QUOTE_ASCII_CHAR: toAppendTo.append((char)count); break; case TAG_QUOTE_CHARS: toAppendTo.append(compiledPattern, i, count); i += count; break; default: subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols); break; } } return toAppendTo; }
當(dāng)看到高亮的那行代碼時,就基本可以認(rèn)定 SimpleDateFormat.format() 不是線程安全的:在多線程環(huán)境下,每次調(diào)用 format 方法時,calendar 都會將時間設(shè)置為傳入的時間,這樣如果上一個線程的 format 方法還在執(zhí)行中,肯定會導(dǎo)致輸出的結(jié)果不正確。
如何做到線程安全
那么怎么做才能線程安全呢?
不用靜態(tài)屬性
每次需要格式化日期時,new 一個 DateFormat,保證線程安全,我們可以這樣“優(yōu)化”一下
String inputValue = game.getId() + new SimpleDateFormat( "yyyyMMdd" ).format( new Date() );
雖然線程安全了,但是這樣性能就下降了,如果這段代碼頻繁的調(diào)用,不僅 new 一個 DateFormat 對象的代價有點(diǎn)大,而且頻繁的 new 也會導(dǎo)致 gc 的壓力增大
同步或者加鎖
既然 format() 方法線程不安全,那就在調(diào)用時采用同步或加鎖,例如
private static SimpleDateFormat df = new SimpleDateFormat( "yyyyMMdd" );// 設(shè)置日期格式 private static final Lock LOCK = new ReentrantLock(); @Override public void addGame( Game game ) { ...... LOCK.lock(); try { String inputValue = game.getId() + df.format( new Date() ); } finally { LOCK.unlock(); } ...... }
DateFormat 池
加鎖是解決了線程安全的問題,也不會因?yàn)?new 出太多的 DateFormat 增加 gc 的負(fù)擔(dān),但是很顯然并發(fā)性能大大的降低了,如果在并發(fā)性要求較高的場合,還不如 new 一個的性能高呢,那有什么辦法再優(yōu)化一下嗎
可以實(shí)現(xiàn)一個 DateFormat 池,實(shí)現(xiàn)如下功能
1. 設(shè)定池的大小,在池實(shí)例化的同時,實(shí)例化一批 DateFormat
2. 每個 DateFormat 都有一個狀態(tài),表明當(dāng)前是空閑還是忙,初始狀態(tài)為空閑
3. 當(dāng)需要格式化日期時,從池里取出一個空閑的 DateFormat,同時將其標(biāo)記為忙
4. 池里的 DateFormat 的 format 方法需要重寫,在 format 完成后將其狀態(tài)標(biāo)記為空閑
用池的優(yōu)勢很明顯:只要設(shè)定合適的池大小,就不用擔(dān)心并發(fā)性能;并且也不存在增加 gc 負(fù)擔(dān)的問題;當(dāng)然池的實(shí)現(xiàn)比較復(fù)雜,而且池內(nèi)部也要解決線程安全問題,可以考慮采用一些開源的池框架例如 Apache commons pool 來做
ThreadLocal<DateFormat>
在引入 ThreadLocal 之前,思考一下這個問題:如果只有一個線程,format 方法還存在線程安全問題嗎? 顯然不會,如果只有一個線程,那么 format 方法實(shí)際上是串行執(zhí)行的,絕無可能并行執(zhí)行;那么,如果在多線程環(huán)境下,給每個線程都分配一個固定的 DateFormat 來執(zhí)行 format 方法,顯然也不會有線程安全的問題了 ThreadLocal 就是用于這種思路的一個解決方案:為每個使用該變量的線程提供獨(dú)立的變量副本,所以每一個線程都可以獨(dú)立地改變自己的副本,而不會影響其它線程所對應(yīng)的副本。 ThreadLocal 的使用如下
private static final String DATE_FORMAT = "yyyy-MM-dd"; private static final ThreadLocal<DateFormat> SDF = new ThreadLocal<DateFormat>() { protected synchronized DateFormat initialValue() { return new SimpleDateFormat( DATE_FORMAT); } }; ...... album.setReleaseDate(SDF.get().parse(show.getReleasedate().substring(0, 10))); // 發(fā)布日期
先看每次都 new 一個新對象和 ThreaLocal之間的差別,假設(shè)有 1 個線程要執(zhí)行 n 次 format
1. new:n 個實(shí)例
2. ThreadLocal:1 個實(shí)例.但是,如果是 n 個線程,每個線程只執(zhí)行 1 次 format 呢?
new:n 個實(shí)例
ThreadLocal:n 個實(shí)例
很顯然,如果使用 ThreadLocal 的話,線程應(yīng)該是能復(fù)用的,否則和 new 的效果是一樣滴
再看加鎖和池的差別,主要表現(xiàn)在并發(fā)性能方面,加鎖實(shí)際導(dǎo)致了串行化,不滿足高并發(fā)的場合
再看下池和 ThreadLocal的差別
3. 并發(fā)性能上2者都很優(yōu)秀
4. 相對來說 ThreadLocal 的實(shí)現(xiàn)更簡單,而且完全不需要加鎖或同步,而池在管理池內(nèi)的元素時免不了需要加鎖或同步
5. 池的復(fù)用性最好,而 ThreadLocal 的復(fù)用性則完全取決于其宿主線程的復(fù)用性
到此這篇關(guān)于Java中的SimpleDateFormat的線程安全問題詳解的文章就介紹到這了,更多相關(guān)SimpleDateFormat線程安全問題內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用mybatis的@Interceptor實(shí)現(xiàn)攔截sql的方法詳解
攔截器是一種基于 AOP(面向切面編程)的技術(shù),它可以在目標(biāo)對象的方法執(zhí)行前后插入自定義的邏輯,本文給大家介紹了使用mybatis的@Interceptor實(shí)現(xiàn)攔截sql的方法,需要的朋友可以參考下2024-03-03Spring中propagation的7種事務(wù)配置及說明
這篇文章主要介紹了Spring中propagation的7種事務(wù)配置及說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06java中的HashSet與 == 和 equals的區(qū)別示例解析
HashSet是Java中基于哈希表實(shí)現(xiàn)的集合類,特點(diǎn)包括:元素唯一、無序和可包含null,本文給大家介紹java中的HashSet與 == 和 equals的區(qū)別,感興趣的朋友一起看看吧2025-02-02java?MongoDB實(shí)現(xiàn)列表分頁查詢的示例代碼
本文主要介紹了java?MongoDB實(shí)現(xiàn)列表分頁查詢的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(26)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望可以幫到你2021-07-07