SimpleDateFormat( 下面简称 sdf) 类内部有一个 Calendar 对象引用 , 它用来储存和这个 sdf 相关的日期信息 , 例如 sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关 String, Date 等等 , 都是交友 Calendar 引用来储存的 . 这样就会导致一个问题 , 如果你的 sdf 是个 static 的 , 那么多个 thread 之间就会共享这个 sdf, 同时也是共享这个 Calendar 引用 , 并且 , 观察 sdf.parse() 方法 , 你会发现有如下的调用 :
1 Date parse() {2 calendar.clear(); // 清理calendar3 ... // 执行一些操作, 设置 calendar 的日期什么的4 calendar.getTime(); // 获取calendar的时间5 }
这里会导致的问题就是 , 如果 线程 A 调用了 sdf.parse(), 并且进行了 calendar.clear() 后还未执行 calendar.getTime() 的时候 , 线程 B 又调用了 sdf.parse(), 这时候线程 B 也执行了 sdf.clear() 方法 , 这样就导致线程 A 的的 calendar 数据被清空了 ( 实际上 A,B 的同时被清空了 ). 又或者当 A 执行了 calendar.clear() 后被挂起 , 这时候 B 开始调用 sdf.parse() 并顺利 i 结束 , 这样 A 的 calendar 内存储的的 date 变成了后来 B 设置的 calendar 的 date
这个问题背后隐藏着一个更为重要的问题 -- 无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。 format 方法在运行过程中改动了SimpleDateFormat 的 calendar 字段,所以,它是有状态的。
这也同时提醒我们在开发和设计系统的时候注意下一下三点 :
1. 自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明
2. 对线程环境下,对每一个共享的可变变量都要注意其线程安全性
3. 我们的类和方法在做设计的时候,要尽量设计成无状态的
1. 需要的时候创建新实例:
说明:在需要用到 SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。
2. 使用同步:同步 SimpleDateFormat 对象
[url=][/url]
1 public class DateSyncUtil { 2 private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 3 4 public static String formatDate(Date date)throws ParseException{ 5 synchronized(sdf){ 6 return sdf.format(date); 7 } 8 } 9 10 public static Date parse(String strDate) throws ParseException{11 synchronized(sdf){12 return sdf.parse(strDate);13 }14 } 15 }[url=][/url]
说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block ,多线程并发量大的时候会对性能有一定的影响。3. 使用 ThreadLocal :
[url=][/url]
1 public class ConcurrentDateUtil { 2 private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { 3 @Override 4 protected DateFormat initialValue() { 5 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 6 } 7 }; 8 public static Date parse(String dateStr) throws ParseException { 9 return threadLocal.get().parse(dateStr);10 }11 public static String format(Date date) {12 return threadLocal.get().format(date);13 }14 }[url=][/url]
或
[url=][/url]
1 ThreadLocal<DateFormat>(); 2 3 public static DateFormat getDateFormat() 4 { 5 DateFormat df = threadLocal.get(); 6 if(df==null){ 7 df = new SimpleDateFormat(date_format); 8 threadLocal.set(df); 9 } 10 return df; 11 } 12 public static String formatDate(Date date) throws ParseException {13 return getDateFormat().format(date);14 }15 public static Date parse(String strDate) throws ParseException {16 return getDateFormat().parse(strDate);17 } 18 }[url=][/url]
说明:使用 ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。
4. 抛弃 JDK ,使用其他类库中的时间格式化类:
1. 使用 Apache commons 里的 FastDateFormat ,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行 format, 不能对日期串进行解析。
2. 使用 Joda-Time 类库来处理时间相关问题
做一个简单的压力测试,方法一最慢,方法三最快,但是就算是最慢的方法一性能也不差,一般系统方法一和方法二就可以满足,所以说在这个点很难成为你系统的瓶颈所在。从简单的角度来说,建议使用方法一或者方法二,如果在必要的时候,追求那么一点性能提升的话,可以考虑用方法三,用 ThreadLocal 做缓存。 |