高并发下非线程安全SimDateFormate导致的生产事故

本篇主要讲解的是高并发下线程安全,使用的讲解例子是项目生产环境所发生的故障。

导致bug的代码简单说下,由于项目中的一个日期时间格式化的工具类,工具类内部使用SimpleDateFormat的无参构造器创建一个静态对象。在工具类的静态方法中,使用SimpleDateFormat的formatter.applyPattern进行格式化。类似下面的代码

1
2
3
4
5
6
public static SimpleDateFormat formatter = new SimpleDateFormat();
public static Date compareDate(String date1,String patter) throws ParseException {
formatter.applyPattern(patter);
Date date = formatter.parse(date1);
return date;
}

在业务代码中使用,但是由于项目比较老,对接多方系统,导致系统中对于日期格式的转化并未统一,存在不同的格式的数据需要格式化。在高并发的情况下,直接导致部分日期解析不正确,并抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Long.parseLong(Long.java:431)
at java.lang.Long.parseLong(Long.java:468)
at java.text.DigitList.getLong(DigitList.java:177)
at java.text.DecimalFormat.parse(DecimalFormat.java:1297)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.xxxxxxx.core.common.util.DateUtil.parseTimestamp(DateUtil.java:95)
at com.xxxxxxx.core.common.util.DateUtil.parse(DateUtil.java:84)
at com.xxxxxxx.hbase.generator.LogRowKeyGenerator.generate(LogRowKeyGenerator.java:21)
... 22 more

但是此问题并不容易重现,我们在对测试环境以及预发布环境都进行压测,并未重现此问题。

后续通过多方排查,才确认是SimpleDateFormat导致。

后经过整理,提出以下几种解决方案

方案一

写一个单独的方法,然后将 SimpleDateFormat 作为方法的成员变量,每个线程需要格式化时间的时候,就去调用这个方法,SimpleDateFormat 作为方法的成员变量,自然就不存在资源共享的问题了

1
2
3
4
5
public static String getCurrentTime(String format) {
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format, Locale.getDefault());
return simpleDateFormat.format(date);
}
方案二

使用==synchronized==,但是在高并发下会存在性能问题,亲测。

方案三

使用ThreadLocal将共享变量变为线程独享

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static final Object lockerObj = new Object();
private static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>();


private static SimpleDateFormat getFormatter(final String pattern) {
ThreadLocal<SimpleDateFormat> threadLocal = sdfMap.get(pattern);
if (threadLocal == null) {
synchronized (lockerObj) {
threadLocal = sdfMap.get(pattern);
if (threadLocal == null) {
threadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(pattern);
}
};
sdfMap.put(pattern, threadLocal);
}
}
}
return threadLocal.get();
}
方案四

抛弃JDK,使用其他类库中的时间格式化类:推荐Apache commons 里的FastDateForma 以及 Joda-Time类库

而我们项目中使用的是Joda-Time,具体使用请自行查看官方文档