SimpleDateFormat在多線程下的安全問(wèn)題及解決
情景重現(xiàn)
SimpleDateFormat類是Java開發(fā)中的一個(gè)日期時(shí)間的轉(zhuǎn)化類。它可以滿足絕大多數(shù)的開發(fā)場(chǎng)景,但是在高并發(fā)下會(huì)出現(xiàn)并發(fā)問(wèn)題。
接下來(lái)查看下文中的案例。
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();
}
}
}上面代碼簡(jiǎn)單來(lái)說(shuō)就是創(chuàng)建了一個(gè)SimpleDateFormat類對(duì)象,該對(duì)象被后續(xù)會(huì)被五個(gè)線程使用,去轉(zhuǎn)化日期格式并打印。
我們來(lái)查看輸出結(jié)果:
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
可以看到,只輸出了一次時(shí)間轉(zhuǎn)化,并且該輸出格式還是錯(cuò)誤的。
接下來(lái)我們來(lái)查看為什么SimpleDateFormat類是線程不安全的。
SimpleDateFormat解析
我們根據(jù)parse()方法,查看SimpleDateFormat是如何進(jìn)行格式轉(zhuǎn)換的。

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

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

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

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

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

此時(shí)我們或許大概明白是因?yàn)镾impleDateFormat類之所以線程不安全的問(wèn)題是因?yàn)樵诙嗑€程下共享了calendar對(duì)象。接下來(lái)我們繼續(xù)查看establish()方法,驗(yàn)證是否是這樣,下面是具體源碼
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;
}可以看到,在該方法中,對(duì)cal對(duì)象執(zhí)行了clear()方法與set()方法,我們查看clear方法是做什么的

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

而set()方法將本次的格式轉(zhuǎn)換需要的數(shù)據(jù)更新。因此,我們可以確定了SimpleDateFormat類之所以線程不安全就是因?yàn)楣蚕砹薱alendar對(duì)象。
解決方案
為了避免SimpleDateFormat格式轉(zhuǎn)換帶來(lái)的并發(fā)問(wèn)題,我們可以采取以下幾個(gè)措施
局部變量
我們已經(jīng)知道了產(chǎn)生線程安全問(wèn)題的原因是共享了相同屬性,那么我們只要讓每個(gè)線程都包含自己的屬性就可以避免該問(wèn)題的發(fā)生。
具體實(shí)現(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();
}
}
}運(yùn)行結(jié)果如下:
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
這種方式不太推薦,因?yàn)闀?huì)創(chuàng)建大量的SimpleDateFormat對(duì)象,占用內(nèi)存空間。
加鎖
除了讓每個(gè)線程都擁有自己獨(dú)立的對(duì)象外,我們也可以保證在同一時(shí)刻下,只有一個(gè)線程對(duì)共享屬性進(jìn)行修改,那就是加鎖。具體實(shí)現(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();
}
}
}但是這種方法不太推薦,因?yàn)槟軌虺霈F(xiàn)格式轉(zhuǎn)化錯(cuò)誤的情況已經(jīng)是很大的并發(fā)了,如果還使用同步鎖的話會(huì)影響性能。
使用線程變量
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之后提供了線程安全的格式轉(zhuǎn)化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();
}
}
}輸出結(jié)果為:
{},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
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java中判斷字符串?dāng)?shù)組是否包含特定字符串的三種方法實(shí)現(xiàn)與對(duì)比
這篇文章主要為大家詳細(xì)介紹了java中判斷字符串?dāng)?shù)組是否包含特定字符串的三種方法實(shí)現(xiàn)與對(duì)比,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2025-12-12
Java?導(dǎo)出Excel增加下拉框選項(xiàng)
這篇文章主要介紹了Java?導(dǎo)出Excel增加下拉框選項(xiàng),excel對(duì)于下拉框較多選項(xiàng)的,需要使用隱藏工作簿來(lái)解決,使用函數(shù)取值來(lái)做選項(xiàng),下文具體的操作詳情,需要的小伙伴可以參考一下2022-04-04
spring boot使用thymeleaf為模板的基本步驟介紹
Spring Boot項(xiàng)目的默認(rèn)模板引擎是Thymeleaf,這沒什么好說(shuō)的,個(gè)人覺得也非常好,下面這篇文章主要給大家介紹了關(guān)于spring boot使用thymeleaf為模板的相關(guān)資料,需要的朋友可以參考借鑒,下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01
Java Socket聊天室編程(二)之利用socket實(shí)現(xiàn)單聊聊天室
這篇文章主要介紹了Java Socket聊天室編程(二)之利用socket實(shí)現(xiàn)單聊聊天室的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09
使用Jmeter進(jìn)行http接口測(cè)試的詳細(xì)流程
本文主要針對(duì)http接口進(jìn)行測(cè)試,使用Jmeter工具實(shí)現(xiàn), Jmter工具設(shè)計(jì)之初是用于做性能測(cè)試的,它在實(shí)現(xiàn)對(duì)各種接口的調(diào)用方面已經(jīng)做的比較成熟,因此,本次直接使用Jmeter工具來(lái)完成對(duì)Http接口的測(cè)試,需要的朋友可以參考下2024-12-12
java中ThreadLocal和ThreadLocalMap淺析
這篇文章主要介紹了java中ThreadLocal和ThreadLocalMap淺析,ThreadLocal類用來(lái)設(shè)置線程私有變量?本身不儲(chǔ)存值?主要提供自身引用?和?操作ThreadLocalMap?屬性值得方法,需要的朋友可以參考下2023-09-09
Springboot轉(zhuǎn)發(fā)重定向?qū)崿F(xiàn)方式解析
這篇文章主要介紹了springboot轉(zhuǎn)發(fā)重定向?qū)崿F(xiàn)方式解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03
Java經(jīng)典面試題匯總:網(wǎng)絡(luò)編程
本篇總結(jié)的是Java 網(wǎng)絡(luò)編程相關(guān)的面試題,后續(xù)會(huì)持續(xù)更新,希望我的分享可以幫助到正在備戰(zhàn)面試的實(shí)習(xí)生或者已經(jīng)工作的同行,如果發(fā)現(xiàn)錯(cuò)誤還望大家多多包涵,不吝賜教,謝謝2021-07-07
解讀java.lang.Character.isLetterOrDigit()的使用方式
這篇文章主要介紹了解讀java.lang.Character.isLetterOrDigit()的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06

