https://mp.weixin.qq.com/s/2uzr800WYtu4R0hycfGruAjava
轉自公衆號:Hollis安全
在平常開發中,咱們常常會用到時間相關類,咱們有不少辦法在Java代碼中獲取時間。可是不一樣的方法獲取到的時間的格式都不盡相同,這時候就須要一種格式化工具,把時間顯示成咱們須要的格式。多線程
最經常使用的方法就是使用SimpleDateFormat類。這是一個看上去功能比較簡單的類,可是,一旦使用不當也有可能致使很大的問題。併發
在阿里巴巴Java開發手冊中,有以下明確規定:ide
那麼,本文就圍繞SimpleDateFormat的用法、原理等來深刻分析下如何以正確的姿式使用它。工具
SimpleDateFormat用法SimpleDateFormat是Java提供的一個格式化和解析日期的工具類。它容許進行格式化(日期 -> 文本)、解析(文本 -> 日期)和規範化。SimpleDateFormat 使得能夠選擇任何用戶定義的日期-時間格式的模式。ui
在Java中,可使用SimpleDateFormat的format方法,將一個Date類型轉化成String類型,而且能夠指定輸出格式。spa
// Date轉String
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);
以上代碼,轉換的結果是:2018-11-25 13:00:00,日期和時間格式由」日期和時間模式」字符串指定。若是你想要轉換成其餘格式,只要指定不一樣的時間模式就好了。線程
在Java中,可使用SimpleDateFormat的parse方法,將一個String類型轉化成Date類型。code
// String轉Data
System.out.println(sdf.parse(dataStr));
在使用SimpleDateFormat的時候,須要經過字母來描述時間元素,並組裝成想要的日期和時間模式。經常使用的時間元素和字母的對應表以下:
模式字母一般是重複的,其數量肯定其精確表示。以下表是經常使用的輸出格式的表示方法。
時區是地球上的區域使用同一個時間定義。之前,人們經過觀察太陽的位置(時角)決定時間,這就使得不一樣經度的地方的時間有所不一樣(地方時)。1863年,首次使用時區的概念。時區經過設立一個區域的標準時間部分地解決了這個問題。
世界各個國家位於地球不一樣位置上,所以不一樣國家,特別是東西跨度大的國家日出、日落時間一定有所誤差。這些誤差就是所謂的時差。
現今全球共分爲24個時區。因爲實用上經常1個國家,或1個省份同時跨着2個或更多時區,爲了照顧到行政上的方便,常將1個國家或1個省份劃在一塊兒。因此時區並不嚴格按南北直線來劃分,而是按天然條件來劃分。例如,中國幅員寬廣,差很少跨5個時區,但爲了使用方便簡單,實際上在只用東八時區的標準時即北京時間爲準。
因爲不一樣的時區的時間是不同的,甚至同一個國家的不一樣城市時間均可能不同,因此,在Java中想要獲取時間的時候,要重點關注一下時區問題。
默認狀況下,若是不指明,在建立日期的時候,會使用當前計算機所在的時區做爲默認時區,這也是爲何咱們經過只要使用new Date()
就能夠獲取中國的當前時間的緣由。
那麼,如何在Java代碼中獲取不一樣時區的時間呢?SimpleDateFormat能夠實現這個功能。
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()));
以上代碼,轉換的結果是: 2018-11-24 21:00:00 。既中國的時間是11月25日的13點,而美國洛杉磯時間比中國北京時間慢了16個小時(這還和冬夏令時有關係,就不詳細展開了)。
若是你感興趣,你還能夠嘗試打印一下美國紐約時間(America/New_York)。紐約時間是2018-11-25 00:00:00。紐約時間比中國北京時間慢了13個小時。
固然,這不是顯示其餘時區的惟一方法,不過本文主要爲了介紹SimpleDateFormat,其餘方法暫不介紹了。
SimpleDateFormat線程安全性因爲SimpleDateFormat比較經常使用,並且在通常狀況下,一個應用中的時間顯示模式都是同樣的,因此不少人願意使用以下方式定義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()));
}
}
這種定義方式,存在很大的安全隱患。
咱們來看一段代碼,如下代碼使用線程池來執行時間輸出。
/** * @author Hollis */
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,保證全部子線程執行完以後主線程再執行
*/
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把時間轉換成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
});
}
//阻塞,直到countDown數量爲0
countDownLatch.await();
//輸出去重後的時間個數
System.out.println(dates.size());
}
}
以上代碼,其實比較容易理解。就是循環一百次,每次循環的時候都在當前時間基礎上增長一個天數(這個天數隨着循環次數而變化),而後把全部日期放入一個線程安全的、帶有去重功能的Set中,而後輸出Set中元素個數。
上面的例子我特地寫的稍微複雜了一些,不過我幾乎都加了註釋。這裏面涉及到了線程池的建立、CountDownLatch、lambda表達式、線程安全的HashSet等知識。感興趣的朋友能夠逐一瞭解一下。
正常狀況下,以上代碼輸出結果應該是100。可是實際執行結果是一個小於100的數字。
緣由就是由於SimpleDateFormat做爲一個非線程安全的類,被當作了共享變量在多個線程中進行使用,這就出現了線程安全問題。
在阿里巴巴Java開發手冊的第一章第六節——併發處理中關於這一點也有明確說明:
那麼,接下來咱們就來看下究竟是爲何,以及該如何解決。
經過以上代碼,咱們發現了在併發場景中使用SimpleDateFormat會有線程安全問題。其實,JDK文檔中已經明確代表了SimpleDateFormat不該該用在多線程場景中:
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.
那麼接下來分析下爲何會出現這種問題,SimpleDateFormat底層究竟是怎麼實現的?
咱們跟一下SimpleDateFormat類中format方法的實現其實就能發現端倪。

SimpleDateFormat中的format方法在執行過程當中,會使用一個成員變量calendar來保存時間。這其實就是問題的關鍵。
因爲咱們在聲明SimpleDateFormat的時候,使用的是static定義的。那麼這個SimpleDateFormat就是一個共享變量,隨之,SimpleDateFormat中的calendar也就能夠被多個線程訪問到。
假設線程1剛剛執行完calendar.setTime
把時間設置成2018-11-11,還沒等執行完,線程2又執行了calendar.setTime
把時間改爲了2018-12-12。這時候線程1繼續往下執行,拿到的calendar.getTime
獲得的時間就是線程2改過以後的。
除了format方法之外,SimpleDateFormat的parse方法也有一樣的問題。
因此,不要把SimpleDateFormat做爲一個共享變量使用。
如何解決前面介紹過了SimpleDateFormat存在的問題以及問題存在的緣由,那麼有什麼辦法解決這種問題呢?
解決方法有不少,這裏介紹三個比較經常使用的方法。
使用局部變量
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把時間轉換成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
});
}
SimpleDateFormat變成了局部變量,就不會被多個線程同時訪問到了,就避免了線程安全問題。
加同步鎖
除了改爲局部變量之外,還有一種方法你們可能比較熟悉的,就是對於共享變量進行加鎖。
for (int i = 0; i < 100; i++) {
//獲取當前時間
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
//加鎖
synchronized (simpleDateFormat) {
//時間增長
calendar.add(Calendar.DATE, finalI);
//經過simpleDateFormat把時間轉換成字符串
String dateString = simpleDateFormat.format(calendar.getTime());
//把字符串放入Set中
dates.add(dateString);
//countDown
countDownLatch.countDown();
}
});
}
經過加鎖,使多個線程排隊順序執行。避免了併發致使的線程安全問題。
其實以上代碼還有能夠改進的地方,就是能夠把鎖的粒度再設置的小一點,能夠只對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());
固然,以上代碼也有改進空間,就是,其實SimpleDateFormat的建立過程能夠改成延遲加載。這裏就不詳細介紹了。
使用DateTimeFormatter
若是是Java8應用,可使用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);
//日期轉換爲字符串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String nowStr = now .format(format);
System.out.println(nowStr);
總結
本文介紹了SimpleDateFormat的用法,SimpleDateFormat主要能夠在String和Date之間作轉換,還能夠將時間轉換成不一樣時區輸出。同時提到在併發場景中SimpleDateFormat是不能保證線程安全的,須要開發者本身來保證其安全性。
主要的幾個手段有改成局部變量、使用synchronized加鎖、使用Threadlocal爲每個線程單首創建一個和使用Java8中的DateTimeFormatter類代替等。
但願經過此文,你能夠在使用SimpleDateFormat的時候更加駕輕就熟。