SimpleDateFormat线程不安全

现象:

如果多个线程同时调用同一个SimpleDateFormat的实例方法,例如parse(String date),format(Date date)等方法,就可能发生线程不安全的问题:

private final static SimpleDateFormat sdf_d = new SimpleDateFormat(
      "yyyyMMdd");

  public static Date str2Date(String date) {
    Date d = null;
    try {
      d =  sdf_d.parse(date);
    } catch (ParseException e) {
      e.printStackTrace();
    }
    return d;
  }

抛出异常:

Exception in thread "Thread-7" 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:1791)
        at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
        at java.text.DateFormat.parse(DateFormat.java:355)
        at com.wbkit.cobub.utils.DateUtil.str2Date(DateUtil.java:16)
        at com.wbkit.cobub.objects.ParticipationDegreeMarker.<init>(ParticipationDegreeMarker.java:34)
        at com.wbkit.cobub.mapreduce.BackFlowUser.getChecker(BackFlowUser.java:279)
        at com.wbkit.cobub.mapreduce.BackFlowUser.addFilter(BackFlowUser.java:258)
        at com.wbkit.cobub.mapreduce.BackFlowUser.run(BackFlowUser.java:69)

  

分析:

SimpleDateFormate的继承关系:

Format <-- DateFormat <-- SimpleDateFormat

在DateFormat中:

protected Calendar calendar;
protected Numberformat  numberFormat;
/**
     * Formats a Date into a date/time string.
     * @param date the time value to be formatted into a time string.
     * @return the formatted time string.
     */
    public final String format(Date date)
    {
        return format(date, new StringBuffer(),
                      DontCareFieldPosition.INSTANCE).toString();
    }

在SimpleDateFormat.java中,当不同线程调用同时调用format时,calendar的值被改变:

// Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);

        boolean useDateFormatSymbols = useDateFormatSymbols();

        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++];
            }

            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;

            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;

            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
    }

结论:

calendar.setTime(date)这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
  线程1调用format方法,改变了calendar这个字段。
  中断来了。
  线程2开始执行,它也改变了calendar。
  又中断了。
  线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
  分析一下format的实现,我们不难发现,用到成员变量calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。
  这个问题背后隐藏着一个更为重要的问题--无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。

  这也同时提醒我们在开发和设计系统的时候注意下一下三点:

  1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明

  2.对线程环境下,对每一个共享的可变变量都要注意其线程安全性

  3.我们的类和方法在做设计的时候,要尽量设计成无状态的

  

 我们需要使用线程安全的策略让SimpleDateFormat变得线程安全

 实现方法有很多,一种是使用synchronized关键字对线程不安全的方法加锁

  private final static SimpleDateFormat sdf_d = new SimpleDateFormat(
      "yyyyMMdd");
  
  public static synchronized Date str2Date(String date) {
    Date d = null;
    try {
      d =  sdf_d.parse(date);
    } catch (ParseException e) {
      e.printStackTrace();
    }
    return d;
  }

 一种是使用ThreadLocal模式,为每一个线程创建自己的线程副本

private static final ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("dd/MM/yyyy");
        }
    };
 
    public String formatDate(Date input) {
        if (input == null) {
            return null;
        }
 
        return simpleDateFormatThreadLocal.get().format(input);
    }
 
    public Date parseDate(String input) throws ParseException {
        if (input == null) {
            return null;
        }
        return simpleDateFormatThreadLocal.get().parse(input);
    }

 一种是使用joda-time 的java时间支持包

猜你喜欢

转载自oitebody.iteye.com/blog/2239577