SimpleDateFormat線程安全問(wèn)題排查詳解
一. 問(wèn)題現(xiàn)象
運(yùn)營(yíng)部門(mén)反饋使用小程序配置的拉新現(xiàn)金紅包活動(dòng)二維碼,在掃碼后跳轉(zhuǎn)至404頁(yè)面。
二. 原因排查
首先,檢查掃碼后的跳轉(zhuǎn)鏈接地址不是對(duì)應(yīng)二維碼的實(shí)際URL,根據(jù)代碼邏輯推測(cè),可能是accessToken在微信端已失效導(dǎo)致,檢查數(shù)據(jù)發(fā)現(xiàn),數(shù)據(jù)庫(kù)存儲(chǔ)的accessToken過(guò)期時(shí)間為2022-11-29(排查問(wèn)題當(dāng)日為2022-10-08),發(fā)現(xiàn)過(guò)期時(shí)間太長(zhǎng),導(dǎo)致accessToken未刷新導(dǎo)致。
接下來(lái),繼續(xù)排查造成這一問(wèn)題的真正原因。排查日志發(fā)現(xiàn)更新sql語(yǔ)句對(duì)應(yīng)的的過(guò)期時(shí)間與數(shù)據(jù)庫(kù)記錄的一致,推測(cè)賦值代碼存在問(wèn)題,如下。
tokenInfo.setExpireTime(simpleDateFormat.parse(token.getString("expireTime")));
其中,simpleDateFormat在代碼中定義是該類的成員變量。
- 跟蹤代碼后發(fā)現(xiàn)源碼中有明確說(shuō)明SimpleDateFormat不應(yīng)該應(yīng)用于多線程場(chǎng)景下。
Synchronization //SimpleDateFormat中的日期格式化不是同步的。 Date formats are not synchronized. //建議為每個(gè)線程創(chuàng)建獨(dú)立的格式實(shí)例。 It is recommended to create separate format instances for each thread. //如果多個(gè)線程同時(shí)訪問(wèn)一個(gè)格式,則它必須保持外部同步。 If multiple threads access a format concurrently, it must be synchronized externally.
- 至此,基本可以判斷是simpleDateFormat.parse在多線程情況下造成錯(cuò)誤的過(guò)期時(shí)間入庫(kù),導(dǎo)致accesstoken無(wú)法正常更新。
三. 原因分析
- 接下來(lái)寫(xiě)個(gè)測(cè)試類來(lái)模擬:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SimpleDateFormatTest {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 定義線程池
**/
private static final ExecutorService threadPool = new ThreadPoolExecutor(16,
20,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024),
new ThreadFactoryBuilder().setNamePrefix("[線程]").build(),
new ThreadPoolExecutor.AbortPolicy()
);
@SneakyThrows
@Test
public void testParse() {
Set<String> results = Collections.synchronizedSet(new HashSet<>());
// 每個(gè)線程都對(duì)相同字符串執(zhí)行“parse日期字符串”的操作,當(dāng)THREAD_NUMBERS個(gè)線程執(zhí)行完畢后,應(yīng)該有且僅有一個(gè)相同的結(jié)果才是正確的
String initialDateStr = "2022-10-08 18:30:01";
for (int i = 0; i < 20; i++) {
threadPool.execute(() -> {
Date parse = null;
try {
parse = simpleDateFormat.parse(initialDateStr);
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + parse);
});
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
}
}
運(yùn)行結(jié)果如下:
[線程]5---Sat Jan 08 18:30:01 CST 2000
[線程]0---Wed Oct 08 18:30:01 CST 2200
[線程]4---Sat Oct 08 18:30:01 CST 2022
Exception in thread "[線程]3" 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:2089)
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 com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
[線程]6---Sat Oct 08 18:30:01 CST 2022
[線程]11---Wed Mar 15 18:30:01 CST 2045
Exception in thread "[線程]2" java.lang.ArrayIndexOutOfBoundsException: 275
at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2397)
at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2818)
at java.util.Calendar.updateTime(Calendar.java:3393)
at java.util.Calendar.getTimeInMillis(Calendar.java:1782)
at java.util.Calendar.getTime(Calendar.java:1755)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1532)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
[線程]6---Fri Oct 01 18:30:01 CST 8202
[線程]12---Sat Oct 08 18:30:01 CST 2022
Exception in thread "[線程]1" 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:2089)
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 com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
[線程]0---Sat Oct 08 18:30:01 CST 2022
[線程]12---Sat Oct 08 18:30:01 CST 2022
[線程]13---Sat Oct 08 18:30:01 CST 2022
[線程]18---Sat Oct 08 18:30:01 CST 2022
[線程]6---Sat Oct 01 18:30:01 CST 2022
[線程]7---Sat Oct 08 18:30:01 CST 2022
[線程]10---Sat Oct 08 18:30:01 CST 2022
[線程]15---Sat Oct 08 18:00:01 CST 2022
[線程]17---Sat Oct 08 18:30:01 CST 2022
[線程]14---Sat Oct 08 18:30:01 CST 2022
預(yù)期結(jié)果個(gè)數(shù) 1---實(shí)際結(jié)果個(gè)數(shù)7
不僅有的線程結(jié)果不正確,甚至還有一些線程還出現(xiàn)了異常!
- 為什么SimpleDateFormat類不是線程安全的?
SimpleDateFormat繼承了DateFormat,DateFormat內(nèi)部有一個(gè)Calendar對(duì)象的引用,主要用來(lái)存儲(chǔ)和SimpleDateFormat相關(guān)的日期信息。
SimpleDateFormat對(duì)parse()方法的實(shí)現(xiàn)。關(guān)鍵代碼如下:
@Override
public Date parse(String text, ParsePosition pos) {
...省略中間代碼
Date parsedDate;
try {
...
parsedDate = calb.establish(calendar).getTime();
} catch (IllegalArgumentException e) {
...
}
return parsedDate;
}
establish()的實(shí)現(xiàn)如下:
Calendar establish(Calendar cal) {
...省略中間代碼
cal.clear();
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;
}
}
}
...
return cal;
}
在多個(gè)線程共享SimpleDateFormat時(shí),同時(shí)也共享了Calendar引用,在如上代碼中,calendar首先會(huì)進(jìn)行clear()操作,然后進(jìn)行set操作,在多線程情況下,set操作會(huì)覆蓋之前的值,而且在后續(xù)對(duì)日期進(jìn)行操作時(shí),也可能會(huì)因?yàn)閏lear操作被清除導(dǎo)致異常。
四. 解決方案
- 將SimpleDateFormat定義成局部變量,每次使用時(shí)都new一個(gè)新對(duì)象,頻繁創(chuàng)建對(duì)象消耗大,性能影響一些(JDK文檔推薦此做法)
public static Date parse(String strDate) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(strDate);
}
- 維護(hù)一個(gè)SimpleDateFormat實(shí)體,轉(zhuǎn)換方法上使用 Synchronized 保證線程安全:多線程堵塞(并發(fā)大系統(tǒng)不推薦)
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date)throws ParseException{
synchronized(sdf){
return sdf.format(date);
}
}
public static Date parse(String strDate) throws ParseException{
synchronized(sdf){
return sdf.parse(strDate);
}
}
- 使用ThreadLocal : 線程獨(dú)享不堵塞,并且減少創(chuàng)建對(duì)象的開(kāi)銷(如果對(duì)性能要求比較高的情況,推薦這種方式)。
public static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static Date parse(String strDate) throws ParseException {
return threadLocal.get().parse(strDate);
}
- DateTimeFormatter是Java8提供的新的日期時(shí)間API中的類,DateTimeFormatter類是線程安全的,可以在高并發(fā)場(chǎng)景下直接使用。
String dateTimeStr= "2016-10-25 12:00:00";
DateTimeFormatter formatter02 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr,formatter02);
System.out.println(localDateTime);
String format = localDateTime.format(formatter02);
System.out.println(format);
2016-10-25T12:00
2016-10-25 12:00:00
最終,我們根據(jù)實(shí)際情況公共包DateUtil類提供的strConvertDate方法,原理是按照方案1來(lái)解決該問(wèn)題。
以上就是SimpleDateFormat線程安全問(wèn)題排查詳解的詳細(xì)內(nèi)容,更多關(guān)于SimpleDateFormat線程安全排查的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mybatis模糊查詢like語(yǔ)句該如何寫(xiě)
MyBatis模糊查詢通常使用LIKE關(guān)鍵字,結(jié)合concat函數(shù)拼接通配符%實(shí)現(xiàn),在MyBatis配置文件中,通過(guò)#{keyword}傳遞參數(shù),生成帶有通配符的查詢語(yǔ)句,MyBatis-Plus中,通過(guò)LambdaQueryWrapper類和like方法構(gòu)建模糊查詢條件,簡(jiǎn)化查詢操作2024-09-09
openEuler?搭建java開(kāi)發(fā)環(huán)境的詳細(xì)過(guò)程
這篇文章主要介紹了openEuler?搭建java開(kāi)發(fā)環(huán)境,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
Spring Cloud Alibaba 使用 Feign+Sentinel 完成熔斷的示例
這篇文章主要介紹了Spring Cloud Alibaba 使用 Feign+Sentinel 完成熔斷的示例,幫助大家更好的理解和學(xué)習(xí)使用Spring Cloud,感興趣的朋友可以了解下2021-03-03
springboot?vue前后端接口測(cè)試樹(shù)結(jié)點(diǎn)添加功能
這篇文章主要為大家介紹了springboot?vue前后端接口測(cè)試樹(shù)結(jié)點(diǎn)添加功能,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
SpringBoot原理之自動(dòng)配置機(jī)制詳解
Springboot遵循“約定優(yōu)于配置”的原則,使用注解對(duì)一些常規(guī)的配置項(xiàng)做默認(rèn)配置,減少或不使用xml配置,讓你的項(xiàng)目快速運(yùn)行起來(lái),下面這篇文章主要給大家介紹了關(guān)于SpringBoot原理之自動(dòng)配置機(jī)制的相關(guān)資料,需要的朋友可以參考下2021-11-11
java通過(guò)ssh連接服務(wù)器執(zhí)行shell命令詳解及實(shí)例
這篇文章主要介紹了java通過(guò)ssh連接服務(wù)器執(zhí)行shell命令詳解及實(shí)例方法的相關(guān)資料2017-02-02

