非线程安全类SimpleDateFormat

来源:互联网 发布:小鸟云服务器知乎 编辑:程序博客网 时间:2024/05/24 03:45

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)
0 0