面試官一步一步的套路你,為什麼SimpleDateFormat不是線程安全的

面試官: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為什麼可能會導致內存洩漏?




分享到:


相關文章: