為什么阿里巴巴禁止把SimpleDateFormat定義為static類型的?
來自:Hollis(微信號:hollischuang)秋秤,作者:Hollis
在日常開發(fā)中,我們經(jīng)常會用到時間相關(guān)類绍哎,我們有很多辦法在Java代碼中獲取時間鞋真。但是不同的方法獲取到的時間的格式都不盡相同涩咖,這時候就需要一種格式化工具,把時間顯示成我們需要的格式饿肺。
最常用的方法就是使用SimpleDateFormat
類敬辣。這是一個看上去功能比較簡單的類零院,但是,一旦使用不當也有可能導(dǎo)致很大的問題撰茎。
在阿里巴巴Java開發(fā)手冊中龄糊,有如下明確規(guī)定:
那么炫惩,本文就圍繞SimpleDateFormat
的用法阿浓、原理等來深入分析下如何以正確的姿勢使用它芭毙。
1. SimpleDateFormat
用法
SimpleDateFormat
是Java提供的一個格式化和解析日期的工具類。它允許進行格式化(日期 -> 文本)粘咖、解析(文本 -> 日期)和規(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)Date
System.out.println(sdf.parse(dataStr));
2. 日期和時間模式表達方法
在使用SimpleDateFormat
的時候凄诞,需要通過字母來描述時間元素帆谍,并組裝成想要的日期和時間模式轴咱。常用的時間元素和字母的對應(yīng)表如下:
模式字母通常是重復(fù)的,其數(shù)量確定其精確表示坚洽。如下表是常用的輸出格式的表示方法西土。
3. 輸出不同時區(qū)的時間
時區(qū)是地球上的區(qū)域使用同一個時間定義需了。以前肋乍,人們通過觀察太陽的位置(時角)決定時間,這就使得不同經(jīng)度的地方的時間有所不同(地方時)堪伍。1863年杠娱,首次使用時區(qū)的概念谱煤。時區(qū)通過設(shè)立一個區(qū)域的標準時間部分地解決了這個問題刘离。
世界各個國家位于地球不同位置上,因此不同國家硫惕,特別是東西跨度大的國家日出恼除、日落時間必定有所偏差豁辉。這些偏差就是所謂的時差。
現(xiàn)今全球共分為24個時區(qū)气破。由于實用上常常1個國家现使,或1個省份同時跨著2個或更多時區(qū),為了照顧到行政上的方便顽冶,常將1個國家或1個省份劃在一起渗稍。所以時區(qū)并不嚴格按南北直線來劃分团滥,而是按自然條件來劃分灸姊。例如秉溉,中國幅員寬廣召嘶,差不多跨5個時區(qū),但為了使用方便簡單甲喝,實際上在只用東八時區(qū)的標準時即北京時間為準埠胖。
由于不同的時區(qū)的時間是不一樣的直撤,甚至同一個國家的不同城市時間都可能不一樣蜕着,所以承匣,在Java中想要獲取時間的時候,要重點關(guān)注一下時區(qū)問題驱敲。
默認情況下宽闲,如果不指明众眨,在創(chuàng)建日期的時候握牧,會使用當前計算機所在的時區(qū)作為默認時區(qū),這也是為什么我們通過只要使用new Date()
就可以獲取中國的當前時間的原因娩梨。
那么沿腰,如何在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)系,就不詳細展開了)芦缰。
如果你感興趣企巢,你還可以嘗試打印一下美國紐約時間(America/New_York)。紐約時間是2018-11-25 00:00:00让蕾。紐約時間比中國北京時間慢了13個小時浪规。
當然,這不是顯示其他時區(qū)的唯一方法探孝,不過本文主要為了介紹SimpleDateFormat
笋婿,其他方法暫不介紹了。
4. 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()));
}}
這種定義方式元镀,存在很大的安全隱患绍填。
4.1 問題重現(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++) {
//獲取當前時間
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)的時候都在當前時間基礎(chǔ)上增加一個天數(shù)(這個天數(shù)隨著循環(huán)次數(shù)而變化)萝快,然后把所有日期放入一個線程安全的锻霎、帶有去重功能的Set中,然后輸出Set中元素個數(shù)揪漩。
上面的例子我特意寫的稍微復(fù)雜了一些旋恼,不過我?guī)缀醵技恿俗⑨尅_@里面涉及到了線程池的創(chuàng)建奄容、
CountDownLatch
冰更、lambda
表達式产徊、線程安全的HashSet
等知識。感興趣的朋友可以逐一了解一下蜀细。
正常情況下舟铜,以上代碼輸出結(jié)果應(yīng)該是100。但是實際執(zhí)行結(jié)果是一個小于100的數(shù)字奠衔。
原因就是因為SimpleDateFormat
作為一個非線程安全的類谆刨,被當做了共享變量在多個線程中進行使用,這就出現(xiàn)了線程安全問題归斤。
在阿里巴巴Java開發(fā)手冊的第一章第六節(jié)——并發(fā)處理中關(guān)于這一點也有明確說明:
那么痊夭,接下來我們就來看下到底是為什么,以及該如何解決脏里。
4.2 線程不安全原因
通過以上代碼生兆,我們發(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
作為一個共享變量使用蛮放。
5. 如何解決
前面介紹過了SimpleDateFormat
存在的問題以及問題存在的原因缩抡,那么有什么辦法解決這種問題呢?
解決方法有很多包颁,這里介紹三個比較常用的方法瞻想。
5.1 使用局部變量
for (int i = 0; i < 100; i++) {
//獲取當前時間
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
變成了局部變量压真,就不會被多個線程同時訪問到了,就避免了線程安全問題内边。
5.2 加同步鎖
除了改成局部變量以外榴都,還有一種方法大家可能比較熟悉的,就是對于共享變量進行加鎖漠其。
for (int i = 0; i < 100; i++) {
//獲取當前時間
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)致的線程安全問題和屎。
其實以上代碼還有可以改進的地方拴驮,就是可以把鎖的粒度再設(shè)置的小一點,可以只對simpleDateFormat.format
這一行加鎖柴信,這樣效率更高一些套啤。
5.3 使用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());
當然绪氛,以上代碼也有改進空間唆鸡,就是,其實SimpleDateFormat
的創(chuàng)建過程可以改為延遲加載枣察。這里就不詳細介紹了争占。
5.4 使用DateTimeFormatter
如果是Java8應(yīng)用,可以使用DateTimeFormatter
代替SimpleDateFormat
序目,這是一個線程安全的格式化工具類臂痕。就像官方文檔中說的,這個類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);
6. 總結(jié)
本文介紹了SimpleDateFormat
的用法握童,SimpleDateFormat
主要可以在String
和Date
之間做轉(zhuǎn)換,還可以將時間轉(zhuǎn)換成不同時區(qū)輸出叛赚。同時提到在并發(fā)場景中SimpleDateFormat
是不能保證線程安全的舆瘪,需要開發(fā)者自己來保證其安全性。
主要的幾個手段有改為局部變量红伦、使用synchronized
加鎖躲因、使用Threadlocal
為每一個線程單獨創(chuàng)建一個和使用Java8中的DateTimeFormatter
類代替等拱她。
希望通過此文昧港,你可以在使用SimpleDateFormat
的時候更加得心應(yīng)手竭讳。