SimpleDateFormat不是線程安全的(thread safe)。這意味着,下面的代碼在多線程環境下運行結果並非如我們所願 - 有時候,它輸出正確的日期,有時候會輸出錯誤的(例如.Tue Aug 11 00:00:00 CST 48201),有些時候甚至會拋出NumberFormatException!!!(當然,在單線程環境是,這段代碼是完全沒有問題的)
打開JDK的源碼,在format方法里,有這樣一段代碼:
calendar.setTime(date);
其中,calendar是DateFormat的protected字段。這條語句改變了calendar,稍后,calendar還會用到(在subFormat方法里),而這就是引發問題的根源。
想象一下,在一個多線程環境下,有兩個線程持有了同一個SimpleDateFormat的實例,分別調用format方法:
- 線程1調用format方法,改變了calendar這個字段。
- 中斷來了。
- 線程2開始執行,它也改變了calendar。
- 又中斷了。
- 線程1回來了,此時,calendar已然不是它所設的值,而是走上了線程2設計的道路。
- BANG!!! 稍微花點時間分析一下format的實現,我們便不難發現,用到calendar,唯一的好處,就是在調用subFormat時,少了一個參數,卻帶來了這許多的問題。其實,只要在這里用一個局部變量,一路傳遞下去,所有問題都將迎刃而解。
這個問題背后隱藏着一個更為重要的問題:無狀態。
無狀態方法的好處之一,就是它在各種環境下,都可以安全的調用。衡量一個方法是否是有狀態的,就看它是否改動了其它的東西,比如全局變量,比如實例的字段。format方法在運行過程中改動了SimpleDateFormat的calendar字段,所以,它是有狀態的。
所以,寫程序,我們要盡量編寫無狀態方法。
我們如何在多線程中的使用DateFormat呢?
1. 同步
最簡單的方法就是在做日期轉換之前,為DateFormat對象加鎖。這種方法使得一次只能讓一個線程訪問DateFormat對象,而其他線程只能等待。
public class DateFormatTest { private final DateFormat format = new SimpleDateFormat("yyyyMMdd"); public Date convert(String source) throws ParseException { synchronized(format) { return format.parse(source); } } }
2. 使用ThreadLocal
另外一個方法就是使用ThreadLocal變量去容納DateFormat對象,也就是說每個線程都有一個屬於自己的副本,並無需等待其他線程去釋放它。這種方法會比使用同步塊更高效。
public class DateFormatTest { private final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyyMMdd"); } }; public Date convert(String source) throws ParseException { return df.get().parse(source); } }
3. Joda-Time
Joda-Time 是一個很棒的開源的 JDK 的日期和日歷 API 的替代品,其 DateTimeFormat 是線程安全而且不變的。
package com.uppower.test; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.Date; public class DateFormatTest { private final DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyyMMdd"); public Date convert(String source){ DateTime d = fmt.parseDateTime(source); returnd.toDate(); } }
4.使用臨時變量(不推薦)
作為一個專業程序員,我們當然知道,相比於共享一個變量的開銷要比每次創建小。創建一個實例來獲取日期格式會比較高效,因為系統不需要多次獲取本地語言和國家。
public class DateFormatTest { public Date convert(String source) throws ParseException { DateFormat format = new SimpleDateFormat("yyyyMMdd"); return format.parse(source); } }