SimpleDateFormat在多線程下的安全問題及解決
情景重現(xiàn)
SimpleDateFormat類是Java開發(fā)中的一個日期時間的轉化類。它可以滿足絕大多數(shù)的開發(fā)場景,但是在高并發(fā)下會出現(xiàn)并發(fā)問題。
接下來查看下文中的案例。
public class TestSimpleDateFormat {
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
Date parse = format.parse("2003-01-01");
System.out.println(parse);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}上面代碼簡單來說就是創(chuàng)建了一個SimpleDateFormat類對象,該對象被后續(xù)會被五個線程使用,去轉化日期格式并打印。
我們來查看輸出結果:
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.lambda$main$0(test.java:21)
at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.lambda$main$0(test.java:21)
at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.lambda$main$0(test.java:21)
at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at test.lambda$main$0(test.java:21)
at java.lang.Thread.run(Thread.java:745)
Fri Nov 01 00:00:00 CST 2222
可以看到,只輸出了一次時間轉化,并且該輸出格式還是錯誤的。
接下來我們來查看為什么SimpleDateFormat類是線程不安全的。
SimpleDateFormat解析
我們根據(jù)parse()方法,查看SimpleDateFormat是如何進行格式轉換的。

我們可以看到,返回結果是根據(jù)另一個方法獲取到的,接下來我們接著查看該parse()源碼。

這是一個抽象方法,接著我們?nèi)ゲ榭此木唧w實現(xiàn)。

可以看到該方法很長,但是我們只關注返回如何結果,直接拉到最后查看該方法如何返回一個日期格式 。上圖中,最后一次修改parseDate對象是在箭頭的位置。那么我們查看getTime()方法。

可以看到該方法是由Calendar類提供的,該類名翻譯為中文就是日歷的意思,并且返回結果也是我們需要的日期格式,那么我們就可以確定該方法用于給parseDate對象提供返回值的,接下來回退一下查看其他方法哪個是提供Calendar對象來調用getTime()方法的。

現(xiàn)在我們清楚了Calendar類對象是由establish()方法提供的了,該方法中需要一個參數(shù)calendar對象。該對象由SimpleDateFormat的父類DateFormat來維護。

此時我們或許大概明白是因為SimpleDateFormat類之所以線程不安全的問題是因為在多線程下共享了calendar對象。接下來我們繼續(xù)查看establish()方法,驗證是否是這樣,下面是具體源碼
Calendar establish(Calendar cal) {
boolean weekDate = isSet(WEEK_YEAR)
&& field[WEEK_YEAR] > field[YEAR];
if (weekDate && !cal.isWeekDateSupported()) {
// Use YEAR instead
if (!isSet(YEAR)) {
set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
}
weekDate = false;
}
cal.clear();
// Set the fields from the min stamp to the max stamp so that
// the field resolution works in the Calendar.
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
for (int index = 0; index <= maxFieldIndex; index++) {
if (field[index] == stamp) {
cal.set(index, field[MAX_FIELD + index]);
break;
}
}
}
if (weekDate) {
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
int dayOfWeek = isSet(DAY_OF_WEEK) ?
field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
if (dayOfWeek >= 8) {
dayOfWeek--;
weekOfYear += dayOfWeek / 7;
dayOfWeek = (dayOfWeek % 7) + 1;
} else {
while (dayOfWeek <= 0) {
dayOfWeek += 7;
weekOfYear--;
}
}
dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
}
cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
}
return cal;
}可以看到,在該方法中,對cal對象執(zhí)行了clear()方法與set()方法,我們查看clear方法是做什么的

該方法提供了類似初始化的功能,將上一次的格式轉化保存的cal屬性清除。

而set()方法將本次的格式轉換需要的數(shù)據(jù)更新。因此,我們可以確定了SimpleDateFormat類之所以線程不安全就是因為共享了calendar對象。
解決方案
為了避免SimpleDateFormat格式轉換帶來的并發(fā)問題,我們可以采取以下幾個措施
局部變量
我們已經(jīng)知道了產(chǎn)生線程安全問題的原因是共享了相同屬性,那么我們只要讓每個線程都包含自己的屬性就可以避免該問題的發(fā)生。
具體實現(xiàn)代碼如下:
public class TestSimpleDateFormat {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
Date parse = format.parse("2003-01-01");
System.out.println(parse);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}運行結果如下:
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
這種方式不太推薦,因為會創(chuàng)建大量的SimpleDateFormat對象,占用內(nèi)存空間。
加鎖
除了讓每個線程都擁有自己獨立的對象外,我們也可以保證在同一時刻下,只有一個線程對共享屬性進行修改,那就是加鎖。具體實現(xiàn)代碼如下
public class TestSimpleDateFormat{
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
Date parse;
synchronized (format){
parse = format.parse("2003-01-01");
}
System.out.println(parse);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}但是這種方法不太推薦,因為能夠出現(xiàn)格式轉化錯誤的情況已經(jīng)是很大的并發(fā)了,如果還使用同步鎖的話會影響性能。
使用線程變量
public class test {
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
Date parse = threadLocal.get().parse("2023-01-01");
System.out.println(parse);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}使用DateTimeFormatter
在JDK8之后提供了線程安全的格式轉化DateTimeFormatter類,使用方法如下
public class test {
public static void main(String[] args) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
TemporalAccessor parse = dateTimeFormatter.parse("2003-06-03");
System.out.println(parse);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}輸出結果為:
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
java中判斷字符串數(shù)組是否包含特定字符串的三種方法實現(xiàn)與對比
這篇文章主要為大家詳細介紹了java中判斷字符串數(shù)組是否包含特定字符串的三種方法實現(xiàn)與對比,文中的示例代碼講解詳細,感興趣的小伙伴可以了解下2025-12-12
spring boot使用thymeleaf為模板的基本步驟介紹
Spring Boot項目的默認模板引擎是Thymeleaf,這沒什么好說的,個人覺得也非常好,下面這篇文章主要給大家介紹了關于spring boot使用thymeleaf為模板的相關資料,需要的朋友可以參考借鑒,下面來一起學習學習吧。2018-01-01
Java Socket聊天室編程(二)之利用socket實現(xiàn)單聊聊天室
這篇文章主要介紹了Java Socket聊天室編程(二)之利用socket實現(xiàn)單聊聊天室的相關資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-09-09
java中ThreadLocal和ThreadLocalMap淺析
這篇文章主要介紹了java中ThreadLocal和ThreadLocalMap淺析,ThreadLocal類用來設置線程私有變量?本身不儲存值?主要提供自身引用?和?操作ThreadLocalMap?屬性值得方法,需要的朋友可以參考下2023-09-09
Springboot轉發(fā)重定向實現(xiàn)方式解析
這篇文章主要介紹了springboot轉發(fā)重定向實現(xiàn)方式解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-03-03
解讀java.lang.Character.isLetterOrDigit()的使用方式
這篇文章主要介紹了解讀java.lang.Character.isLetterOrDigit()的使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06

