為什么阿里巴巴禁止把SimpleDateFormat定義為static類型的?

在日常開發(fā)中,我們經(jīng)常會用到時間相關(guān)類,我們有很多辦法在Java代碼中獲取時間。但是不同的方法獲取到的時間的格式都不盡相同,這時候就需要一種格式化工具,把時間顯示成我們需要的格式。

最常用的方法就是使用SimpleDateFormat類。這是一個看上去功能比較簡單的類,但是,一旦使用不當(dāng)也有可能導(dǎo)致很大的問題。

在阿里巴巴Java開發(fā)手冊中,有如下明確規(guī)定:

image

那么,本文就圍繞SimpleDateFormat的用法、原理等來深入分析下如何以正確的姿勢使用它。

SimpleDateFormat用法

SimpleDateFormat是Java提供的一個格式化和解析日期的工具類。它允許進(jìn)行格式化(日期 -> 文本)、解析(文本 -> 日期)和規(guī)范化。SimpleDateFormat 使得可以選擇任何用戶定義的日期-時間格式的模式。

在Java中,可以使用SimpleDateFormat的format方法,將一個Date類型轉(zhuǎn)化成String類型,并且可以指定輸出格式。

// Date轉(zhuǎn)StringDate data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);

以上代碼,轉(zhuǎn)換的結(jié)果是:2018-11-25 13:00:00,日期和時間格式由”日期和時間模式”字符串指定。如果你想要轉(zhuǎn)換成其他格式,只要指定不同的時間模式就行了。

在Java中,可以使用SimpleDateFormat的parse方法,將一個String類型轉(zhuǎn)化成Date類型。

// String轉(zhuǎn)DataSystem.out.println(sdf.parse(dataStr));

日期和時間模式表達(dá)方法

在使用SimpleDateFormat的時候,需要通過字母來描述時間元素,并組裝成想要的日期和時間模式。常用的時間元素和字母的對應(yīng)表如下:

image

模式字母通常是重復(fù)的,其數(shù)量確定其精確表示。如下表是常用的輸出格式的表示方法。

image

輸出不同時區(qū)的時間

時區(qū)是地球上的區(qū)域使用同一個時間定義。以前,人們通過觀察太陽的位置(時角)決定時間,這就使得不同經(jīng)度的地方的時間有所不同(地方時)。1863年,首次使用時區(qū)的概念。時區(qū)通過設(shè)立一個區(qū)域的標(biāo)準(zhǔn)時間部分地解決了這個問題。

世界各個國家位于地球不同位置上,因此不同國家,特別是東西跨度大的國家日出、日落時間必定有所偏差。這些偏差就是所謂的時差。

現(xiàn)今全球共分為24個時區(qū)。由于實用上常常1個國家,或1個省份同時跨著2個或更多時區(qū),為了照顧到行政上的方便,常將1個國家或1個省份劃在一起。所以時區(qū)并不嚴(yán)格按南北直線來劃分,而是按自然條件來劃分。例如,中國幅員寬廣,差不多跨5個時區(qū),但為了使用方便簡單,實際上在只用東八時區(qū)的標(biāo)準(zhǔn)時即北京時間為準(zhǔn)。

由于不同的時區(qū)的時間是不一樣的,甚至同一個國家的不同城市時間都可能不一樣,所以,在Java中想要獲取時間的時候,要重點關(guān)注一下時區(qū)問題。

默認(rèn)情況下,如果不指明,在創(chuàng)建日期的時候,會使用當(dāng)前計算機(jī)所在的時區(qū)作為默認(rèn)時區(qū),這也是為什么我們通過只要使用new Date()就可以獲取中國的當(dāng)前時間的原因。

那么,如何在Java代碼中獲取不同時區(qū)的時間呢?SimpleDateFormat可以實現(xiàn)這個功能。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));

以上代碼,轉(zhuǎn)換的結(jié)果是: 2018-11-24 21:00:00 。既中國的時間是11月25日的13點,而美國洛杉磯時間比中國北京時間慢了16個小時(這還和冬夏令時有關(guān)系,就不詳細(xì)展開了)。

如果你感興趣,你還可以嘗試打印一下美國紐約時間(America/New_York)。紐約時間是2018-11-25 00:00:00。紐約時間比中國北京時間慢了13個小時。

當(dāng)然,這不是顯示其他時區(qū)的唯一方法,不過本文主要為了介紹SimpleDateFormat,其他方法暫不介紹了。

SimpleDateFormat線程安全性

由于SimpleDateFormat比較常用,而且在一般情況下,一個應(yīng)用中的時間顯示模式都是一樣的,所以很多人愿意使用如下方式定義SimpleDateFormat:

public class Main {   
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
 public static void main(String[] args) {
 simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York")); 
 System.out.println(simpleDateFormat.format(Calendar.getInstance().getTime()));   
}}

這種定義方式,存在很大的安全隱患。

問題重現(xiàn)

我們來看一段代碼,以下代碼使用線程池來執(zhí)行時間輸出。

public class Main {   
/* 定義一個全局的SimpleDateFormat */   
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");   /** 使用ThreadFactoryBuilder定義一個線程池 */   
private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS,  new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());   
/* 定義一個CountDownLatch,保證所有子線程執(zhí)行完之后主線程再執(zhí)行 */   
private static CountDownLatch countDownLatch = new CountDownLatch(100);   public static void main(String[] args) {
//定義一個線程安全的HashSet       Set<String> dates = Collections.synchronizedSet(new HashSet<String>());       
for (int i = 0; i < 100; i++) {           
//獲取當(dāng)前時間           
Calendar calendar = Calendar.getInstance();          
 int finalI = i;           
pool.execute(() -> {                   
//時間增加                   
calendar.add(Calendar.DATE, finalI);                   
//通過simpleDateFormat把時間轉(zhuǎn)換成字符串                   
String dateString = simpleDateFormat.format(calendar.getTime());                   
//把字符串放入Set中                   
dates.add(dateString);                   
//countDown                   
countDownLatch.countDown();           
});       
}       
//阻塞,直到countDown數(shù)量為0       
countDownLatch.await();       
//輸出去重后的時間個數(shù)       
System.out.println(dates.size());   
}}

以上代碼,其實比較容易理解。就是循環(huán)一百次,每次循環(huán)的時候都在當(dāng)前時間基礎(chǔ)上增加一個天數(shù)(這個天數(shù)隨著循環(huán)次數(shù)而變化),然后把所有日期放入一個線程安全的、帶有去重功能的Set中,然后輸出Set中元素個數(shù)。

上面的例子我特意寫的稍微復(fù)雜了一些,不過我?guī)缀醵技恿俗⑨?。這里面涉及到了線程池的創(chuàng)建、CountDownLatch、lambda表達(dá)式、線程安全的HashSet等知識。感興趣的朋友可以逐一了解一下。

正常情況下,以上代碼輸出結(jié)果應(yīng)該是100。但是實際執(zhí)行結(jié)果是一個小于100的數(shù)字。

原因就是因為SimpleDateFormat作為一個非線程安全的類,被當(dāng)做了共享變量在多個線程中進(jìn)行使用,這就出現(xiàn)了線程安全問題。

在阿里巴巴Java開發(fā)手冊的第一章第六節(jié)——并發(fā)處理中關(guān)于這一點也有明確說明:

image

那么,接下來我們就來看下到底是為什么,以及該如何解決。

線程不安全原因

通過以上代碼,我們發(fā)現(xiàn)了在并發(fā)場景中使用SimpleDateFormat會有線程安全問題。其實,JDK文檔中已經(jīng)明確表明了SimpleDateFormat不應(yīng)該用在多線程場景中:

Date formats are not synchronized.

It is recommended to create separate format instances for each thread.

If multiple threads access a format concurrently, it must be synchronized externally.

那么接下來分析下為什么會出現(xiàn)這種問題,SimpleDateFormat底層到底是怎么實現(xiàn)的?

我們跟一下SimpleDateFormat類中format方法的實現(xiàn)其實就能發(fā)現(xiàn)端倪。

SimpleDateFormat中的format方法在執(zhí)行過程中,會使用一個成員變量calendar來保存時間。這其實就是問題的關(guān)鍵。

由于我們在聲明SimpleDateFormat的時候,使用的是static定義的。那么這個SimpleDateFormat就是一個共享變量,隨之,SimpleDateFormat中的calendar也就可以被多個線程訪問到。

假設(shè)線程1剛剛執(zhí)行完calendar.setTime把時間設(shè)置成2018-11-11,還沒等執(zhí)行完,線程2又執(zhí)行了calendar.setTime把時間改成了2018-12-12。這時候線程1繼續(xù)往下執(zhí)行,拿到的calendar.getTime得到的時間就是線程2改過之后的。

除了format方法以外,SimpleDateFormat的parse方法也有同樣的問題。

所以,不要把SimpleDateFormat作為一個共享變量使用。

如何解決

前面介紹過了SimpleDateFormat存在的問題以及問題存在的原因,那么有什么辦法解決這種問題呢?

解決方法有很多,這里介紹三個比較常用的方法。

使用局部變量

for (int i = 0; i < 100; i++) {
   //獲取當(dāng)前時間   Calendar calendar = Calendar.getInstance();
   int finalI = i;   pool.execute(() -> {       // SimpleDateFormat聲明成局部變量
  SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");    
   //時間增加    
   calendar.add(Calendar.DATE, finalI);    
   //通過simpleDateFormat把時間轉(zhuǎn)換成字符串     
  String dateString = simpleDateFormat.format(calendar.getTime());    
   //把字符串放入Set中    
   dates.add(dateString);    
   //countDown    
   countDownLatch.countDown();
   });
}

SimpleDateFormat變成了局部變量,就不會被多個線程同時訪問到了,就避免了線程安全問題。

加同步鎖

除了改成局部變量以外,還有一種方法大家可能比較熟悉的,就是對于共享變量進(jìn)行加鎖。

for (int i = 0; i < 100; i++) {  
 //獲取當(dāng)前時間   Calendar calendar = Calendar.getInstance();   int finalI = i;   pool.execute(() -> {     
  //加鎖    
  synchronized (simpleDateFormat) {         
  //時間增加         
  calendar.add(Calendar.DATE, finalI);         
  //通過simpleDateFormat把時間轉(zhuǎn)換成字符串        
  String dateString = simpleDateFormat.format(calendar.getTime());       
   //把字符串放入Set中        
   dates.add(dateString);     
    //countDown        
   countDownLatch.countDown();
       }   
    });
}

通過加鎖,使多個線程排隊順序執(zhí)行。避免了并發(fā)導(dǎo)致的線程安全問題。

其實以上代碼還有可以改進(jìn)的地方,就是可以把鎖的粒度再設(shè)置的小一點,可以只對simpleDateFormat.format這一行加鎖,這樣效率更高一些。

使用ThreadLocal

第三種方式,就是使用 ThreadLocal。 ThreadLocal 可以確保每個線程都可以得到單獨的一個 SimpleDateFormat 的對象,那么自然也就不存在競爭問題了。

/*** 使用ThreadLocal定義一個全局的SimpleDateFormat*/
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {   
@Override   
protected SimpleDateFormat initialValue() {       return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 }};
//用法String 
dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());

當(dāng)然,以上代碼也有改進(jìn)空間,就是,其實SimpleDateFormat的創(chuàng)建過程可以改為延遲加載。這里就不詳細(xì)介紹了。

使用DateTimeFormatter

如果是Java8應(yīng)用,可以使用DateTimeFormatter代替SimpleDateFormat,這是一個線程安全的格式化工具類。就像官方文檔中說的,這個類 simple beautiful strongimmutable thread-safe。

//解析日期String 
dateStr= "2016年10月25日";DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate date= LocalDate.parse(dateStr, formatter);
//日期轉(zhuǎn)換為字符串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String nowStr = now .format(format);
System.out.println(nowStr);
總結(jié)

本文介紹了SimpleDateFormat的用法,SimpleDateFormat主要可以在String和Date之間做轉(zhuǎn)換,還可以將時間轉(zhuǎn)換成不同時區(qū)輸出。同時提到在并發(fā)場景中SimpleDateFormat是不能保證線程安全的,需要開發(fā)者自己來保證其安全性。

主要的幾個手段有改為局部變量、使用synchronized加鎖、使用Threadlocal為每一個線程單獨創(chuàng)建一個和使用Java8中的DateTimeFormatter類代替等。

希望通過此文,你可以在使用SimpleDateFormat的時候更加得心應(yīng)手。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內(nèi)容