现象:
如果多个线程同时调用同一个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.我们的类和方法在做设计的时候,要尽量设计成无状态的
线程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时间支持包