面試官:SimpleDateFormat用過嗎?
小小白:用過,用來格式化或解析日期時間。
面試官:能寫一下你是如何使用的嗎?
小小白:噼裡啪啦敲完了,代碼如下:
public class SimpleDateFormatTest {
static final ThreadLocal<dateformat> df = new ThreadLocal<dateformat>() {/<dateformat>/<dateformat>
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static void main(String[] args) {
try {
df.get().parse("2020-03-27 10:09:01");
} catch (ParseException e) {
e.printStackTrace();
} finally {
df.remove();
}
}
}
面試官:為什麼要使用ThreadLocal包裝SimpleDateFormat?
小小白:如果不使用ThreadLocal包裝一下,直接創建一個SimpleDateFormat共享實例對象,在多線程併發的情況下使用這個對象的方法是線程不安全的,可能會拋出NumberFormatException或其它異常。使用ThreadLocal包裝一下,每個線程都有自己的SimpleDateFormat實例對象,這樣多線程併發的情況下就不會出現線程不安全的問題了。
面試官:那為什麼SimpleDateFormat作為共享變量會出現線程不安全的問題?
小小白:以下面的代碼為例,說明一下原因。
public class SimpleDateFormatTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
public static void main(String[] args) {
for (int i = 0; i <= 15; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(sdf.parse("2020-3-27 10:09:01"));
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
SimpleDateFormat類繼承了DateFormat抽象類,在DateFormat抽象類中有一個Calendar類型的屬性calendar,它就是導致SimpleDateFormat線程不安全的關鍵。上面代碼中變量sdf是全局共享的,首先通過SimpleDateFormat構造方法創建對象,進入這個構造方法的源碼。
public SimpleDateFormat(String pattern)
{
this(pattern, Locale.getDefault(Locale.Category.FORMAT));
}
public SimpleDateFormat(String pattern, Locale locale)
{
if (pattern == null || locale == null) {
throw new NullPointerException();
}
// 初始化一個Calendar實例
initializeCalendar(locale);
this.pattern = pattern;
this.formatData = DateFormatSymbols.getInstanceRef(locale);
this.locale = locale;
initialize(locale);
}
// 如果DateFormat中calendar屬性等於null,則創建一個
private void initializeCalendar(Locale loc) {
if (calendar == null) {
assert loc != null;
// The format object must be constructed using the symbols for this zone.
// However, the calendar should use the current default TimeZone.
// If this is not contained in the locale zone strings, then the zone
// will be formatted using generic GMT+/-H:MM nomenclature.
calendar = Calendar.getInstance(TimeZone.getDefault(), loc);
}
}
在這個構造方法中初始化了一些SimpleDateFormat的屬性值,特別是calendar。接著,進入SimpleDateFormat類的parse方法源碼。
public Date parse(String source) throws ParseException
{
ParsePosition pos = new ParsePosition(0);
// 通過參數值source解析得到一個Date
Date result = parse(source, pos);
if (pos.index == 0)
throw new ParseException("Unparseable date: \"" + source + "\"" ,
pos.errorIndex);
return result;
}
// 通過參數值source解析得到一個Date
// 這個方法代碼很多,這裡省略不重要的代碼
public Date parse(String text, ParsePosition pos)
{
// 省略解析日期字符串的部分代碼
CalendarBuilder calb = new CalendarBuilder();
// 日期字符串解析完成後存放到calb中
Date parsedDate;
try {
// 使用calb中解析好的日期數據設置calendar
parsedDate = calb.establish(calendar).getTime();
// 省略部分代碼
}
catch (IllegalArgumentException e) {
// 省略部分代碼
}
return parsedDate;
}
// CalendarBuilder類的establish方法部分代碼
Calendar establish(Calendar cal) {
// 省略部分不重要代碼
cal.clear();
// 省略部分不重要代碼
return cal;
}
從上面的代碼可以看到,根據傳入的日期時間字符串來解析,然後將解析好的日期數據設置到calendar中,也就是通過establish方法完成的,注意看這個方法中調用了cal.clear(),這將會導致calendar中的屬性值變為初始值,如果在多線程併發的情況下,有可能線程A剛執行完establish方法,線程B就執行了cal.clear(),導致最終的解析異常。
面試官:那既然是因為SimpleDateFormat類中的calendar是共享變量導致的,可以將SimpleDateFormat類型的變量設置成方法私有的,每次要用的時候new一個不就完了,或者使用同步鎖控制一下併發訪問,為什麼還要使用ThreadLocal這麼麻煩?
小小白:方法私有和同步鎖併發控制確實可以解決問題,但是他們有各自的缺點。方法私有,每次都需要創建一個新對象,這樣佔用內存不說,還需要不斷回收。同步鎖併發控制,多線程高併發的情況下,性能會嚴重下降。使用ThreadLocal包裝,每個線程只需要創建一個SimpleDateFormat對象實例,既解決不斷創建新對象的問題,又解決了併發的問題。
面試官:那為什麼最開始你寫的代碼中,在finally方法中使用了df.remove(),這是為什麼?
小小白:線程用完了SimpleDateFormat,如果不調用remove方法將其清除,可能會引發因使用ThreadLocal而導致的內存洩漏。
面試官:使用ThreadLocal為什麼可能會導致內存洩漏?
閱讀更多 Java實戰技術 的文章