淺析Java中SimpleDateFormat為什么是線程不安全的
在日常開(kāi)發(fā)中,Date工具類使用頻率相對(duì)較高,大家通常都會(huì)這樣寫:
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); }
這很簡(jiǎn)單啊,有什么爭(zhēng)議嗎
你應(yīng)該聽(tīng)過(guò)“時(shí)區(qū)”這個(gè)名詞,大家也都知道,相同時(shí)刻不同時(shí)區(qū)的時(shí)間是不一樣的。
因此在使用時(shí)間時(shí),一定要給出時(shí)區(qū)信息。
public static void getDataByZone(String param, String format) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(format); // 默認(rèn)時(shí)區(qū)解析時(shí)間表示 Date date = sdf.parse(param); System.out.println(date + ":" + date.getTime()); // 東京時(shí)區(qū)解析時(shí)間表示 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"); }
對(duì)于當(dāng)前的上海時(shí)區(qū)和紐約時(shí)區(qū),轉(zhuǎn)化為 UTC 時(shí)間戳是不同的時(shí)間。
對(duì)于同一個(gè)本地時(shí)間的表示,不同時(shí)區(qū)的人解析得到的 UTC 時(shí)間一定是不同的,反過(guò)來(lái)不同的本地時(shí)間可能對(duì)應(yīng)同一個(gè) UTC。
格式化后出現(xiàn)的時(shí)間錯(cuò)亂。
public static void getDataByZoneFormat(String param, String format) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat(format); Date date = sdf.parse(param); // 默認(rèn)時(shí)區(qū)格式化輸出 System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date)); // 東京時(shí)區(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)前時(shí)區(qū)的 Offset(時(shí)差)是 +8 小時(shí),對(duì)于 +9 小時(shí)的紐約,整整差了1個(gè)小時(shí),北京早上 10 點(diǎn)對(duì)應(yīng)早上東京 11 點(diǎn)。
看看Java 8是如何解決時(shí)區(qū)問(wèn)題的:
Java 8 推出了新的時(shí)間日期類 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter,處理時(shí)區(qū)問(wèn)題更簡(jiǎn)單清晰。
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è)置時(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對(duì)應(yīng)+8,對(duì)應(yīng)2023-11-10 10:00:00;
- Asia/Tokyo對(duì)應(yīng)+9,對(duì)應(yīng)2023-11-10 11:00:00;
- timeZone 是+2,所以對(duì)應(yīng)2023-11-10 04:00:00;
在處理帶時(shí)區(qū)的國(guó)際化時(shí)間問(wèn)題,推薦使用jdk8的日期時(shí)間類:
- 通過(guò)ZoneId,定義時(shí)區(qū);
- 使用ZonedDateTime保存時(shí)間;
- 通過(guò)withZone對(duì)DateTimeFormatter設(shè)置時(shí)區(qū);
- 進(jìn)行時(shí)間格式化得到本地時(shí)間;
思路比較清晰,不容易出錯(cuò)。
在與前端聯(lián)調(diào)時(shí),報(bào)了個(gè)錯(cuò),java.lang.NumberFormatException: multiple points,起初我以為是時(shí)間格式傳的不對(duì),仔細(xì)一看,不對(duì)啊。
百度一下,才知道是高并發(fā)情況下SimpleDateFormat有線程安全的問(wèn)題。
下面通過(guò)模擬高并發(fā),把這個(gè)問(wèn)題復(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)境,開(kāi)啟5個(gè)并發(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); }
果不其然,報(bào)錯(cuò)。還將2023年轉(zhuǎn)換成2220年,我勒個(gè)乖乖。
在時(shí)間工具類里,時(shí)間格式化,我都是這樣弄的啊,沒(méi)問(wèn)題啊,為啥這個(gè)不行?原來(lái)是因?yàn)楣灿昧送粋€(gè)SimpleDateFormat,在工具類里,一個(gè)線程一個(gè)SimpleDateFormat,當(dāng)然沒(méi)問(wèn)題啦!
可以通過(guò)TreadLocal 局部變量,解決SimpleDateFormat的線程安全問(wèn)題。
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)境,開(kāi)啟5個(gè)并發(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();
- 通過(guò)parsedDate = calb.establish(calendar).getTime();解析時(shí)間;
- establish方法內(nèi)先cal.clear(),再重新構(gòu)建cal,整個(gè)操作沒(méi)有加鎖;
上面幾步就會(huì)導(dǎo)致在高并發(fā)場(chǎng)景下,線程1正在操作一個(gè)Calendar,此時(shí)線程2又來(lái)了。線程1還沒(méi)來(lái)得及處理 Calendar 就被線程2清空了。
因此,通過(guò)編寫Date工具類,一個(gè)線程一個(gè)SimpleDateFormat,還是有一定道理的。
以上就是淺析Java中SimpleDateFormat為什么是線程不安全的的詳細(xì)內(nèi)容,更多關(guān)于Java SimpleDateFormat線程不安全的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java8中Optional操作的實(shí)際應(yīng)用
Optional類是一個(gè)可以為null的容器對(duì)象,如果值存在則isPresent()方法會(huì)返回true,調(diào)用get()方法會(huì)返回該對(duì)象,下面這篇文章主要給大家介紹了關(guān)于Java8中Optional操作實(shí)際應(yīng)用的相關(guān)資料,需要的朋友可以參考下2022-02-02springmvc Rest風(fēng)格介紹及實(shí)現(xiàn)代碼示例
這篇文章主要介紹了springmvc Rest風(fēng)格介紹及實(shí)現(xiàn)代碼示例,rest風(fēng)格簡(jiǎn)潔,分享了HiddenHttpMethodFilter 的源碼,通過(guò)Spring4.0實(shí)現(xiàn)rest風(fēng)格源碼及簡(jiǎn)單錯(cuò)誤分析,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11java設(shè)計(jì)模式之代理模式(Porxy)詳解
這篇文章主要為大家詳細(xì)介紹了java設(shè)計(jì)模式之代理模式Porxy的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Spring?Boot與Spring?MVC?Spring對(duì)比及核心概念
這篇文章主要為大家介紹了Spring?Boot與Spring?MVC?Spring的對(duì)比以及你需要了解的核心概念,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-03-03