1. Problem phenomenon
The operation department feeds back the QR code of the new cash red envelope activity configured by the applet, and jumps to the 404 page after scanning the code.
2. Cause investigation
-
First of all, check that the jump link address after scanning the code is not the actual URL corresponding to the QR code. According to the logic of the code, it may be that the accessToken has expired on the WeChat side. The inspection data shows that the expiration time of the accessToken stored in the database is 2022-11- 29 (the day of troubleshooting is 2022-10-08), it is found that the expiration time is too long, resulting in the accessToken not being refreshed.
-
Next, continue to investigate the real cause of this problem. The troubleshooting log found that the expiration time corresponding to the update sql statement is consistent with the database record. It is speculated that there is a problem with the assignment code, as follows.
tokenInfo.setExpireTime(simpleDateFormat.parse(token.getString("expireTime")));
Among them, simpleDateFormat is defined in the code as a member variable of the class.
- After tracking the code, I found that the source code clearly states that SimpleDateFormat should not be used in multi-threaded scenarios.
Synchronization //Date formatting in SimpleDateFormat is not synchronous. Date formats are not synchronized. //It is recommended to create separate format instances for each thread. It is recommended to create separate format instances for each thread. //If multiple threads access a format at the same time, it must maintain external synchronization. If multiple threads access a format concurrently, it must be synchronized externally.
- So far, it can be basically judged that simpleDateFormat.parse caused the wrong expiration time to be stored in the multi-threaded situation, resulting in the failure of the accesstoken to be updated normally.
3. Reason Analysis
- Next, write a test class to simulate:
@RunWith(SpringRunner.class) @SpringBootTest public class SimpleDateFormatTest { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** * Define thread pool **/ private static final ExecutorService threadPool = new ThreadPoolExecutor(16, 20, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(1024), new ThreadFactoryBuilder().setNamePrefix("[thread]").build(), new ThreadPoolExecutor.AbortPolicy() ); @SneakyThrows @Test public void testParse() { Set<String> results = Collections.synchronizedSet(new HashSet<>()); // Each thread performs the "parse date string" operation on the same string. When THREAD_NUMBERS threads are executed, there should be one and only one identical result to be correct. 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); } }
The results are as follows:
[thread]5---Sat Jan 08 18:30:01 CST 2000 [thread]0---Wed Oct 08 18:30:01 CST 2200 [thread]4---Sat Oct 08 18:30:01 CST 2022 Exception in thread "[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) [thread]6---Sat Oct 08 18:30:01 CST 2022 [thread]11---Wed Mar 15 18:30:01 CST 2045 Exception in thread "[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) [thread]6---Fri Oct 01 18:30:01 CST 8202 [thread]12---Sat Oct 08 18:30:01 CST 2022 Exception in thread "[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) [thread]0---Sat Oct 08 18:30:01 CST 2022 [thread]12---Sat Oct 08 18:30:01 CST 2022 [thread]13---Sat Oct 08 18:30:01 CST 2022 [thread]18---Sat Oct 08 18:30:01 CST 2022 [thread]6---Sat Oct 01 18:30:01 CST 2022 [thread]7---Sat Oct 08 18:30:01 CST 2022 [thread]10---Sat Oct 08 18:30:01 CST 2022 [thread]15---Sat Oct 08 18:00:01 CST 2022 [thread]17---Sat Oct 08 18:30:01 CST 2022 [thread]14---Sat Oct 08 18:30:01 CST 2022 Expected number of results 1---The actual number of results 7
Not only did some threads have incorrect results, but some threads even had exceptions!
- Why is SimpleDateFormat class not thread safe?
SimpleDateFormat inherits DateFormat, and DateFormat has a reference to a Calendar object, which is mainly used to store date information related to SimpleDateFormat.
SimpleDateFormat's implementation of the parse() method. The key code is as follows:
@Override public Date parse(String text, ParsePosition pos) { ...omit intermediate code Date parsedDate; try { ... parsedDate = calb.establish(calendar).getTime(); } catch (IllegalArgumentException e) { ... } return parsedDate; }
The implementation of establish() is as follows:
Calendar establish(Calendar cal) { ...omit intermediate code 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; }
When multiple threads share SimpleDateFormat, they also share the Calendar reference. In the above code, the calendar will first perform the clear() operation, and then perform the set operation. In the case of multiple threads, the set operation will overwrite the previous value, and in the When subsequent operations are performed on the date, an exception may also be caused by the clear operation being cleared.
4. Solutions
- Define SimpleDateFormat as a local variable, and create a new object every time it is used. Frequent object creation consumes a lot of money and affects performance (JDK documentation recommends this practice)
public static Date parse(String strDate) throws ParseException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(strDate); }
- Maintain a SimpleDateFormat entity, and use Synchronized on the conversion method to ensure thread safety: multi-thread blocking (not recommended for large concurrent systems)
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); } }
- Use ThreadLocal : Thread exclusive use does not block, and reduces the overhead of creating objects (if the performance requirements are relatively high, this method is recommended).
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 is a class in the new date and time API provided by Java8. The DateTimeFormatter class is thread-safe and can be used directly in high concurrency scenarios.
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
Finally, we use the strConvertDate method provided by the public package DateUtil class according to the actual situation, and the principle is to solve the problem according to scheme 1.