運營部門反饋使用小程式設定的拉新現金紅包活動二維條碼,在掃碼後跳轉至404頁面。
首先,檢查掃碼後的跳轉連結地址不是對應二維條碼的實際URL,根據程式碼邏輯推測,可能是accessToken在微信端已失效導致,檢查資料發現,資料庫儲存的accessToken過期時間為2022-11-29(排查問題當日為2022-10-08),發現過期時間太長,導致accessToken未重新整理導致。
接下來,繼續排查造成這一問題的真正原因。排查紀錄檔發現更新sql語句對應的的過期時間與資料庫記錄的一致,推測賦值程式碼存在問題,如下。
tokenInfo.setExpireTime(simpleDateFormat.parse(token.getString("expireTime")));
其中,simpleDateFormat在程式碼中定義是該類的成員變數。
Synchronization
//SimpleDateFormat中的日期格式化不是同步的。
Date formats are not synchronized.
//建議為每個執行緒建立獨立的格式範例。
It is recommended to create separate format instances for each thread.
//如果多個執行緒同時存取一個格式,則它必須保持外部同步。
If multiple threads access a format concurrently, it must be synchronized externally.
@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<>());
// 每個執行緒都對相同字串執行「parse日期字串」的操作,當THREAD_NUMBERS個執行緒執行完畢後,應該有且僅有一個相同的結果才是正確的
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);
}
}
執行結果如下:
[執行緒]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
預期結果個數 1---實際結果個數7
不僅有的執行緒結果不正確,甚至還有一些執行緒還出現了異常!
SimpleDateFormat繼承了DateFormat,DateFormat內部有一個Calendar物件的參照,主要用來儲存和SimpleDateFormat相關的日期資訊。
SimpleDateFormat對parse()方法的實現。關鍵程式碼如下:
@Override
public Date parse(String text, ParsePosition pos) {
...省略中間程式碼
Date parsedDate;
try {
...
parsedDate = calb.establish(calendar).getTime();
} catch (IllegalArgumentException e) {
...
}
return parsedDate;
}
establish()的實現如下:
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;
}
在多個執行緒共用SimpleDateFormat時,同時也共用了Calendar參照,在如上程式碼中,calendar首先會進行clear()操作,然後進行set操作,在多執行緒情況下,set操作會覆蓋之前的值,而且在後續對日期進行操作時,也可能會因為clear操作被清除導致異常。
public static Date parse(String strDate) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(strDate);
}
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);
}
}
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);
}
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
最終,我們根據實際情況公共包DateUtil類提供的strConvertDate方法,原理是按照方案1來解決該問題。