淺析Java中SimpleDateFormat為什么是線程不安全的
在日常開發(fā)中,Date工具類使用頻率相對較高,大家通常都會這樣寫:
public static Date getData(String date) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(date);
}
public static Date getDataByFormat(String date, String format) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.parse(date);
}
這很簡單啊,有什么爭議嗎
你應(yīng)該聽過“時區(qū)”這個名詞,大家也都知道,相同時刻不同時區(qū)的時間是不一樣的。
因此在使用時間時,一定要給出時區(qū)信息。
public static void getDataByZone(String param, String format) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(format);
// 默認(rèn)時區(qū)解析時間表示
Date date = sdf.parse(param);
System.out.println(date + ":" + date.getTime());
// 東京時區(qū)解析時間表示
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
Date newYorkDate = sdf.parse(param);
System.out.println(newYorkDate + ":" + newYorkDate.getTime());
}
public static void main(String[] args) throws ParseException {
getDataByZone("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss");
}
對于當(dāng)前的上海時區(qū)和紐約時區(qū),轉(zhuǎn)化為 UTC 時間戳是不同的時間。
對于同一個本地時間的表示,不同時區(qū)的人解析得到的 UTC 時間一定是不同的,反過來不同的本地時間可能對應(yīng)同一個 UTC。

格式化后出現(xiàn)的時間錯亂。
public static void getDataByZoneFormat(String param, String format) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(format);
Date date = sdf.parse(param);
// 默認(rèn)時區(qū)格式化輸出
System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));
// 東京時區(qū)格式化輸出
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));
}
public static void main(String[] args) throws ParseException {
getDataByZoneFormat("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss");
}
我當(dāng)前時區(qū)的 Offset(時差)是 +8 小時,對于 +9 小時的紐約,整整差了1個小時,北京早上 10 點對應(yīng)早上東京 11 點。

看看Java 8是如何解決時區(qū)問題的:
Java 8 推出了新的時間日期類 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter,處理時區(qū)問題更簡單清晰。
public static void getDataByZoneFormat8(String param, String format) throws ParseException {
ZoneId zone = ZoneId.of("Asia/Shanghai");
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZoneId timeZone = ZoneOffset.ofHours(2);
// 格式化器
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(format);
ZonedDateTime date = ZonedDateTime.of(LocalDateTime.parse(param, dtf), zone);
// withZone設(shè)置時區(qū)
DateTimeFormatter dtfz = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");
System.out.println(dtfz.withZone(zone).format(date));
System.out.println(dtfz.withZone(tokyoZone).format(date));
System.out.println(dtfz.withZone(timeZone).format(date));
}
public static void main(String[] args) throws ParseException {
getDataByZoneFormat8("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss");
}
- Asia/Shanghai對應(yīng)+8,對應(yīng)2023-11-10 10:00:00;
- Asia/Tokyo對應(yīng)+9,對應(yīng)2023-11-10 11:00:00;
- timeZone 是+2,所以對應(yīng)2023-11-10 04:00:00;

在處理帶時區(qū)的國際化時間問題,推薦使用jdk8的日期時間類:
- 通過ZoneId,定義時區(qū);
- 使用ZonedDateTime保存時間;
- 通過withZone對DateTimeFormatter設(shè)置時區(qū);
- 進行時間格式化得到本地時間;
思路比較清晰,不容易出錯。
在與前端聯(lián)調(diào)時,報了個錯,java.lang.NumberFormatException: multiple points,起初我以為是時間格式傳的不對,仔細一看,不對啊。
百度一下,才知道是高并發(fā)情況下SimpleDateFormat有線程安全的問題。
下面通過模擬高并發(fā),把這個問題復(fù)現(xiàn)一下:
public static void getDataByThread(String param, String format) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
SimpleDateFormat sdf = new SimpleDateFormat(format);
// 模擬并發(fā)環(huán)境,開啟5個并發(fā)線程
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
for (int j = 0; j < 2; j++) {
try {
System.out.println(sdf.parse(param));
} catch (ParseException e) {
System.out.println(e);
}
}
});
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
}
果不其然,報錯。還將2023年轉(zhuǎn)換成2220年,我勒個乖乖。
在時間工具類里,時間格式化,我都是這樣弄的啊,沒問題啊,為啥這個不行?原來是因為共用了同一個SimpleDateFormat,在工具類里,一個線程一個SimpleDateFormat,當(dāng)然沒問題啦!

可以通過TreadLocal 局部變量,解決SimpleDateFormat的線程安全問題。
public static void getDataByThreadLocal(String time, String format) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(format);
}
};
// 模擬并發(fā)環(huán)境,開啟5個并發(fā)線程
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
for (int j = 0; j < 2; j++) {
try {
System.out.println(sdf.get().parse(time));
} catch (ParseException e) {
System.out.println(e);
}
}
});
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
}

看一下SimpleDateFormat.parse的源碼:
public class SimpleDateFormat extends DateFormat {
@Override
public Date parse(String text, ParsePosition pos){
CalendarBuilder calb = new CalendarBuilder();
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
// If the year value is ambiguous,
// then the two-digit year == the default start year
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
}
}
class CalendarBuilder {
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;
}
}
}
...
}
}
- 先new CalendarBuilder();
- 通過parsedDate = calb.establish(calendar).getTime();解析時間;
- establish方法內(nèi)先cal.clear(),再重新構(gòu)建cal,整個操作沒有加鎖;
上面幾步就會導(dǎo)致在高并發(fā)場景下,線程1正在操作一個Calendar,此時線程2又來了。線程1還沒來得及處理 Calendar 就被線程2清空了。
因此,通過編寫Date工具類,一個線程一個SimpleDateFormat,還是有一定道理的。
以上就是淺析Java中SimpleDateFormat為什么是線程不安全的的詳細內(nèi)容,更多關(guān)于Java SimpleDateFormat線程不安全的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot3.4.0無法找到StringRedisTemplate?bean的問題Consider?def
本文主要介紹了SpringBoot3.4.0無法找到StringRedisTemplate?bean的問題Consider?defining?a?bean?of?type?‘org.springframework,具有一定的參考價值,感興趣的可以了解一下2025-03-03
一篇文章帶你了解SpringMVC數(shù)據(jù)綁定
這篇文章主要給大家介紹了關(guān)于如何通過一篇文章弄懂Spring MVC的參數(shù)綁定,文中通過示例代碼以及圖文介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2021-08-08
Java Fluent Mybatis 項目工程化與常規(guī)操作詳解流程篇 下
Java中常用的ORM框架主要是mybatis, hibernate, JPA等框架。國內(nèi)又以Mybatis用的多,基于mybatis上的增強框架,又有mybatis plus和TK mybatis等。今天我們介紹一個新的mybatis增強框架 fluent mybatis關(guān)于項目工程化與常規(guī)操作流程2021-10-10
java poi實現(xiàn)Excel多級表頭導(dǎo)出方式(多級表頭,復(fù)雜表頭)
文章介紹了使用javapoi庫實現(xiàn)Excel多級表頭導(dǎo)出的方法,通過主代碼、合并單元格、設(shè)置表頭單元格寬度、填充數(shù)據(jù)、web下載和提供表頭樣式、內(nèi)容樣式以及標(biāo)題樣式等步驟,作者實現(xiàn)了最終的效果2025-01-01

