SimpleDateFormat是非线程安全的,写处理日期的工具类时候请注意。
问题背景:
项目组的同事在新项目里写了一个DateUtil专门处理日期格式化的工具。线上运行后台日志偶然发生莫名其妙的错误:
java.lang.NumberFormatException: multiple points
java.lang.NumberFormatException: For input string: “”
java.lang.NumberFormatException: For input string: “.31023102EE22”
例如:
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
或者
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:453)
at java.lang.Long.parseLong(Long.java:483)
at java.text.DigitList.getLong(DigitList.java:194)
at java.text.DecimalFormat.parse(DecimalFormat.java:1316)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
原因分析:
根据错误日志搜来问题代码:
public class DateUtil {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
...
...
public static Date parse(String strDate) throws ParseException{
return sdf.parse(strDate);
}
}
再分析项目调用该代码的场景:
Service A (线程1)某方法执行DateUtil.parse(“2017-02-12”)
Service B (线程2)某方法执行DateUtil.parse(“2017-03-12”)
并且当service A和service B同时触发上面代码时候就出问题了。
JDK源码分析:
调用链:
SimpleDateFormat里parse(String strDate)
=>DateFormat里parse(source, pos);
public class SimpleDateFormat extends DateFormat {
...
transient private char[] compiledPattern;
...
...
public Date parse(String text, ParsePosition pos)
{
checkNegativeNumberExpression();
int start = pos.index;
int oldStart = start;
int textLength = text.length();
boolean[] ambiguousYear = {false};
CalendarBuilder calb = new CalendarBuilder();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
} ...
...
...
在DateUtil里变量SimpleDateFormat被定义为static,因而所有线程调用DateUtil时候都共享了该变量。
一看源码就直觉知道SimpleDateFormat是一个有状态的对象了,因为它拥有很多成员变量,而且变量和很多方法都没有加锁同步处理。
例如状态变量compiledPattern>>>8这句,假设多个线程同时修改该方法值,那各个线程间就互相影响了,从而SimpleDateFormat的parse方法肯定出问题。
解决方案:
- 去掉全局静态变量SimpleDateFormat,在每个parse方法里new SimpleDateFormat
public class DateUtil {
...
...
public static Date parse(String strDate) throws ParseException{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.parse(strDate);
}
}
- 在parse方法前加synchronized同步
public class DateUtil {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
...
...
public static synchronized Date parse(String strDate) throws ParseException{
return sdf.parse(strDate);
}
}
- 使用线程封闭的ThreadLocal实现同一线程内共享,不同线程间隔离 (此方法不推荐,详细解释留意下一篇文章详解ThreadLocal)