为什么阿里巴巴禁止把SimpleDateFormat定义为static类型的?
在日常開發(fā)中,我們經(jīng)常會(huì)用到時(shí)間相關(guān)類,我們有很多辦法在Java代碼中獲取時(shí)間。但是不同的方法獲取到的時(shí)間的格式都不盡相同,這時(shí)候就需要一種格式化工具,把時(shí)間顯示成我們需要的格式。
最常用的方法就是使用SimpleDateFormat類。這是一個(gè)看上去功能比較簡(jiǎn)單的類,但是,一旦使用不當(dāng)也有可能導(dǎo)致很大的問(wèn)題。
在阿里巴巴Java開發(fā)手冊(cè)中,有如下明確規(guī)定:
那么,本文就圍繞SimpleDateFormat的用法、原理等來(lái)深入分析下如何以正確的姿勢(shì)使用它。
?
SimpleDateFormat用法
SimpleDateFormat是Java提供的一個(gè)格式化和解析日期的工具類。它允許進(jìn)行格式化(日期 -> 文本)、解析(文本 -> 日期)和規(guī)范化。SimpleDateFormat 使得可以選擇任何用戶定義的日期-時(shí)間格式的模式。
在Java中,可以使用SimpleDateFormat的format方法,將一個(gè)Date類型轉(zhuǎn)化成String類型,并且可以指定輸出格式。
// Date轉(zhuǎn)String Date?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,日期和時(shí)間格式由”日期和時(shí)間模式”字符串指定。如果你想要轉(zhuǎn)換成其他格式,只要指定不同的時(shí)間模式就行了。
在Java中,可以使用SimpleDateFormat的parse方法,將一個(gè)String類型轉(zhuǎn)化成Date類型。
// String轉(zhuǎn)Data System.out.println(sdf.parse(dataStr));日期和時(shí)間模式表達(dá)方法
在使用SimpleDateFormat的時(shí)候,需要通過(guò)字母來(lái)描述時(shí)間元素,并組裝成想要的日期和時(shí)間模式。常用的時(shí)間元素和字母的對(duì)應(yīng)表如下:
模式字母通常是重復(fù)的,其數(shù)量確定其精確表示。如下表是常用的輸出格式的表示方法。
輸出不同時(shí)區(qū)的時(shí)間
時(shí)區(qū)是地球上的區(qū)域使用同一個(gè)時(shí)間定義。以前,人們通過(guò)觀察太陽(yáng)的位置(時(shí)角)決定時(shí)間,這就使得不同經(jīng)度的地方的時(shí)間有所不同(地方時(shí))。1863年,首次使用時(shí)區(qū)的概念。時(shí)區(qū)通過(guò)設(shè)立一個(gè)區(qū)域的標(biāo)準(zhǔn)時(shí)間部分地解決了這個(gè)問(wèn)題。
世界各個(gè)國(guó)家位于地球不同位置上,因此不同國(guó)家,特別是東西跨度大的國(guó)家日出、日落時(shí)間必定有所偏差。這些偏差就是所謂的時(shí)差。
現(xiàn)今全球共分為24個(gè)時(shí)區(qū)。由于實(shí)用上常常1個(gè)國(guó)家,或1個(gè)省份同時(shí)跨著2個(gè)或更多時(shí)區(qū),為了照顧到行政上的方便,常將1個(gè)國(guó)家或1個(gè)省份劃在一起。所以時(shí)區(qū)并不嚴(yán)格按南北直線來(lái)劃分,而是按自然條件來(lái)劃分。例如,中國(guó)幅員寬廣,差不多跨5個(gè)時(shí)區(qū),但為了使用方便簡(jiǎn)單,實(shí)際上在只用東八時(shí)區(qū)的標(biāo)準(zhǔn)時(shí)即北京時(shí)間為準(zhǔn)。
由于不同的時(shí)區(qū)的時(shí)間是不一樣的,甚至同一個(gè)國(guó)家的不同城市時(shí)間都可能不一樣,所以,在Java中想要獲取時(shí)間的時(shí)候,要重點(diǎn)關(guān)注一下時(shí)區(qū)問(wèn)題。
默認(rèn)情況下,如果不指明,在創(chuàng)建日期的時(shí)候,會(huì)使用當(dāng)前計(jì)算機(jī)所在的時(shí)區(qū)作為默認(rèn)時(shí)區(qū),這也是為什么我們通過(guò)只要使用new Date()就可以獲取中國(guó)的當(dāng)前時(shí)間的原因。
那么,如何在Java代碼中獲取不同時(shí)區(qū)的時(shí)間呢?SimpleDateFormat可以實(shí)現(xiàn)這個(gè)功能。
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 。既中國(guó)的時(shí)間是11月25日的13點(diǎn),而美國(guó)洛杉磯時(shí)間比中國(guó)北京時(shí)間慢了16個(gè)小時(shí)(這還和冬夏令時(shí)有關(guān)系,就不詳細(xì)展開了)。
如果你感興趣,你還可以嘗試打印一下美國(guó)紐約時(shí)間(America/New_York)。紐約時(shí)間是2018-11-25 00:00:00。紐約時(shí)間比中國(guó)北京時(shí)間慢了13個(gè)小時(shí)。
當(dāng)然,這不是顯示其他時(shí)區(qū)的唯一方法,不過(guò)本文主要為了介紹SimpleDateFormat,其他方法暫不介紹了。
SimpleDateFormat線程安全性
由于SimpleDateFormat比較常用,而且在一般情況下,一個(gè)應(yīng)用中的時(shí)間顯示模式都是一樣的,所以很多人愿意使用如下方式定義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()));} }這種定義方式,存在很大的安全隱患。
問(wèn)題重現(xiàn)
我們來(lái)看一段代碼,以下代碼使用線程池來(lái)執(zhí)行時(shí)間輸出。
/** * @author Hollis */? public?class?Main?{/*** 定義一個(gè)全局的SimpleDateFormat*/private?static?SimpleDateFormat simpleDateFormat =?new?SimpleDateFormat("yyyy-MM-dd HH:mm:ss");/*** 使用ThreadFactoryBuilder定義一個(gè)線程池*/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());/*** 定義一個(gè)CountDownLatch,保證所有子線程執(zhí)行完之后主線程再執(zhí)行*/private?static?CountDownLatch countDownLatch =?new?CountDownLatch(100);public?static?void?main(String[] args)?{//定義一個(gè)線程安全的HashSetSet<String> dates = Collections.synchronizedSet(new?HashSet<String>());for?(int?i =?0; i <?100; i++) {//獲取當(dāng)前時(shí)間Calendar calendar = Calendar.getInstance();int?finalI = i;pool.execute(() -> {//時(shí)間增加calendar.add(Calendar.DATE, finalI);//通過(guò)simpleDateFormat把時(shí)間轉(zhuǎn)換成字符串String dateString = simpleDateFormat.format(calendar.getTime());//把字符串放入Set中dates.add(dateString);//countDowncountDownLatch.countDown();});}//阻塞,直到countDown數(shù)量為0countDownLatch.await();//輸出去重后的時(shí)間個(gè)數(shù)System.out.println(dates.size());} }以上代碼,其實(shí)比較容易理解。就是循環(huán)一百次,每次循環(huán)的時(shí)候都在當(dāng)前時(shí)間基礎(chǔ)上增加一個(gè)天數(shù)(這個(gè)天數(shù)隨著循環(huán)次數(shù)而變化),然后把所有日期放入一個(gè)線程安全的、帶有去重功能的Set中,然后輸出Set中元素個(gè)數(shù)。
上面的例子我特意寫的稍微復(fù)雜了一些,不過(guò)我?guī)缀醵技恿俗⑨尅_@里面涉及到了線程池的創(chuàng)建、CountDownLatch、lambda表達(dá)式、線程安全的HashSet等知識(shí)。感興趣的朋友可以逐一了解一下。
正常情況下,以上代碼輸出結(jié)果應(yīng)該是100。但是實(shí)際執(zhí)行結(jié)果是一個(gè)小于100的數(shù)字。
原因就是因?yàn)镾impleDateFormat作為一個(gè)非線程安全的類,被當(dāng)做了共享變量在多個(gè)線程中進(jìn)行使用,這就出現(xiàn)了線程安全問(wèn)題。
在阿里巴巴Java開發(fā)手冊(cè)的第一章第六節(jié)——并發(fā)處理中關(guān)于這一點(diǎn)也有明確說(shuō)明:
那么,接下來(lái)我們就來(lái)看下到底是為什么,以及該如何解決。
線程不安全原因
通過(guò)以上代碼,我們發(fā)現(xiàn)了在并發(fā)場(chǎng)景中使用SimpleDateFormat會(huì)有線程安全問(wèn)題。其實(shí),JDK文檔中已經(jīng)明確表明了SimpleDateFormat不應(yīng)該用在多線程場(chǎ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.
那么接下來(lái)分析下為什么會(huì)出現(xiàn)這種問(wèn)題,SimpleDateFormat底層到底是怎么實(shí)現(xiàn)的?
我們跟一下SimpleDateFormat類中format方法的實(shí)現(xiàn)其實(shí)就能發(fā)現(xiàn)端倪。
SimpleDateFormat中的format方法在執(zhí)行過(guò)程中,會(huì)使用一個(gè)成員變量calendar來(lái)保存時(shí)間。這其實(shí)就是問(wèn)題的關(guān)鍵。
由于我們?cè)诼暶鱏impleDateFormat的時(shí)候,使用的是static定義的。那么這個(gè)SimpleDateFormat就是一個(gè)共享變量,隨之,SimpleDateFormat中的calendar也就可以被多個(gè)線程訪問(wèn)到。
假設(shè)線程1剛剛執(zhí)行完calendar.setTime把時(shí)間設(shè)置成2018-11-11,還沒(méi)等執(zhí)行完,線程2又執(zhí)行了calendar.setTime把時(shí)間改成了2018-12-12。這時(shí)候線程1繼續(xù)往下執(zhí)行,拿到的calendar.getTime得到的時(shí)間就是線程2改過(guò)之后的。
除了format方法以外,SimpleDateFormat的parse方法也有同樣的問(wèn)題。
所以,不要把SimpleDateFormat作為一個(gè)共享變量使用。
?
如何解決
前面介紹過(guò)了SimpleDateFormat存在的問(wèn)題以及問(wèn)題存在的原因,那么有什么辦法解決這種問(wèn)題呢?
解決方法有很多,這里介紹三個(gè)比較常用的方法。
使用局部變量
for?(int?i =?0; i <?100; i++) {//獲取當(dāng)前時(shí)間Calendar calendar = Calendar.getInstance();int?finalI = i;pool.execute(() -> {// SimpleDateFormat聲明成局部變量SimpleDateFormat simpleDateFormat =?new?SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//時(shí)間增加calendar.add(Calendar.DATE, finalI);//通過(guò)simpleDateFormat把時(shí)間轉(zhuǎn)換成字符串String dateString = simpleDateFormat.format(calendar.getTime());//把字符串放入Set中dates.add(dateString);//countDowncountDownLatch.countDown();}); }SimpleDateFormat變成了局部變量,就不會(huì)被多個(gè)線程同時(shí)訪問(wèn)到了,就避免了線程安全問(wèn)題。
加同步鎖
除了改成局部變量以外,還有一種方法大家可能比較熟悉的,就是對(duì)于共享變量進(jìn)行加鎖。
for?(int?i =?0; i <?100; i++) {//獲取當(dāng)前時(shí)間Calendar calendar = Calendar.getInstance();int?finalI = i;pool.execute(() -> {//加鎖synchronized (simpleDateFormat) {//時(shí)間增加calendar.add(Calendar.DATE, finalI);//通過(guò)simpleDateFormat把時(shí)間轉(zhuǎn)換成字符串String dateString = simpleDateFormat.format(calendar.getTime());//把字符串放入Set中dates.add(dateString);//countDowncountDownLatch.countDown();}}); }通過(guò)加鎖,使多個(gè)線程排隊(duì)順序執(zhí)行。避免了并發(fā)導(dǎo)致的線程安全問(wèn)題。
其實(shí)以上代碼還有可以改進(jìn)的地方,就是可以把鎖的粒度再設(shè)置的小一點(diǎn),可以只對(duì)simpleDateFormat.format這一行加鎖,這樣效率更高一些。
使用ThreadLocal
第三種方式,就是使用 ThreadLocal。 ThreadLocal 可以確保每個(gè)線程都可以得到單獨(dú)的一個(gè) SimpleDateFormat 的對(duì)象,那么自然也就不存在競(jìng)爭(zhēng)問(wèn)題了。
/** * 使用ThreadLocal定義一個(gè)全局的SimpleDateFormat */ private?static?ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal =?new?ThreadLocal<SimpleDateFormat>() {@Overrideprotected?SimpleDateFormat?initialValue()?{return?new?SimpleDateFormat("yyyy-MM-dd HH:mm:ss");} };//用法 String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());當(dāng)然,以上代碼也有改進(jìn)空間,就是,其實(shí)SimpleDateFormat的創(chuàng)建過(guò)程可以改為延遲加載。這里就不詳細(xì)介紹了。
使用DateTimeFormatter
如果是Java8應(yīng)用,可以使用DateTimeFormatter代替SimpleDateFormat,這是一個(gè)線程安全的格式化工具類。就像官方文檔中說(shuō)的,這個(gè)類?simple beautiful strong immutable 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)換,還可以將時(shí)間轉(zhuǎn)換成不同時(shí)區(qū)輸出。同時(shí)提到在并發(fā)場(chǎng)景中SimpleDateFormat是不能保證線程安全的,需要開發(fā)者自己來(lái)保證其安全性。
主要的幾個(gè)手段有改為局部變量、使用synchronized加鎖、使用Threadlocal為每一個(gè)線程單獨(dú)創(chuàng)建一個(gè)和使用Java8中的DateTimeFormatter類代替等。
希望通過(guò)此文,你可以在使用SimpleDateFormat的時(shí)候更加得心應(yīng)手。
總結(jié)
以上是生活随笔為你收集整理的为什么阿里巴巴禁止把SimpleDateFormat定义为static类型的?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Java 中初始化 List 集合的 6
- 下一篇: 一篇文章带你飞,轻松弄懂 CDN 技术原