2017.12.11SimpleDateFormat的线程安全性讨论
转载来自:http://blog.csdn.net/zxh87/article/details/19414885
1.结论
DateFormat和SimpleDateFormat都不是线程安全的。在多线程环境中调用format()和parse()应处理线程安全的问题。
2.错误示例
(1)错误示例1
每次处理一个时间信息,都新建一个SimpleDateFormat实例,然后再丢弃。造成内存的浪费。
1 package com.peidasoft.dateformat; 2 3 import java.text.ParseException; 4 import java.text.SimpleDateFormat; 5 import java.util.Date; 6 7 public class DateUtil { 8 9 public static String formatDate(Date date)throws ParseException{ 10 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 11 return sdf.format(date); 12 } 13 14 public static Date parse(String strDate) throws ParseException{ 15 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 16 return sdf.parse(strDate); 17 } 18 }
(2)错误示例2
为了防止频繁创建,使用静态的SimpleDateFormat实例,所有关于时间的处理都使用这个静态实例。
缺点是:在多线程环境中会有问题,比如转换的时间不对,线程被挂死或者报奇怪的错误等等。都是因为SimpleDateFormat线程不安全造成的。
1 package com.peidasoft.dateformat; 2 3 import java.text.ParseException; 4 import java.text.SimpleDateFormat; 5 import java.util.Date; 6 7 public class DateUtil { 8 private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 9 10 public static String formatDate(Date date)throws ParseException{ 11 return sdf.format(date); 12 } 13 14 public static Date parse(String strDate) throws ParseException{ 16 return sdf.parse(strDate); 17 } 18 }
3.源码
JDK文档中对于DateFormat的说明:
SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。
1 Synchronization: 2 Date formats are not synchronized. 3 It is recommended to create separate format instances for each thread. 4 If multiple threads access a format concurrently, it must be synchronized externally.
SimpleDateFormat继承了DateFormat。在DateFormat中定义了一个Calendar类的对象calendar。因为Calendar累的概念复杂,牵扯到时区与本地化等等,所以Jdk的实现中使用了成员变量来传递参数。
format方法如下:
1 private StringBuffer format(Date date, StringBuffer toAppendTo, 2 FieldDelegate delegate) { 4 calendar.setTime(date); 6 boolean useDateFormatSymbols = useDateFormatSymbols(); 7 8 for (int i = 0; i < compiledPattern.length; ) { 9 int tag = compiledPattern[i] >>> 8; 10 int count = compiledPattern[i++] & 0xff; 11 if (count == 255) { 12 count = compiledPattern[i++] << 16; 13 count |= compiledPattern[i++]; 14 } 15 16 switch (tag) { 17 case TAG_QUOTE_ASCII_CHAR: 18 toAppendTo.append((char)count); 19 break; 20 21 case TAG_QUOTE_CHARS: 22 toAppendTo.append(compiledPattern, i, count); 23 i += count; 24 break; 25 26 default: 27 subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols); 28 break; 29 } 30 } 31 return toAppendTo; 32 }
calendar.setTime(date)这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。
在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
- 线程1调用format方法,改变了calendar这个字段。
- 中断。
- 线程2开始执行,它也改变了calendar。
- 又中断。
- 线程1回来了,此时,calendar已然不是它所设的值,再往下执行就可能会出现错误。
如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
这个问题背后隐藏着一个更为重要的问题--无状态。
无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以它是有状态的。
这也同时提醒我们在开发和设计系统的时候注意下一下三点:
- 自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明。
- 对线程环境下,对每一个共享的可变变量都要注意其线程安全性。
- 我们的类和方法在做设计的时候,要尽量设计成无状态的。
4.解决办法
(1)如果不特别考虑性能,可以采用错误示例1中的用法,每用到一个SimpleDateFormat就新建一个
(2)如果考虑性能,想使用错误示例2中的形式,就需要采取额外的同步措施
1 public class DateSyncUtil { 2 3 private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 4 5 public static String formatDate(Date date)throws ParseException{ 6 synchronized(sdf){ 7 return sdf.format(date); 8 } 9 } 10 11 public static Date parse(String strDate) throws ParseException{ 12 synchronized(sdf){ 13 return sdf.parse(strDate); 14 } 15 } 16 }
(3)如果要更加考虑性能,可以使用ThreadLocal
使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。
写法一:
1 package com.peidasoft.dateformat; 2 3 import java.text.DateFormat; 4 import java.text.ParseException; 5 import java.text.SimpleDateFormat; 6 import java.util.Date; 7 8 public class ConcurrentDateUtil { 9 10 private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { 11 @Override 12 protected DateFormat initialValue() { 13 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 14 } 15 }; 16 17 public static Date parse(String dateStr) throws ParseException { 18 return threadLocal.get().parse(dateStr); 19 } 20 21 public static String format(Date date) { 22 return threadLocal.get().format(date); 23 } 24 }
写法二:
1 public class ThreadLocalDateUtil { 2 private static final String date_format = "yyyy-MM-dd HH:mm:ss"; 3 private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
4 public static DateFormat getDateFormat(){ 6 DateFormat df = threadLocal.get(); 7 if(df==null){ 8 df = new SimpleDateFormat(date_format); 9 threadLocal.set(df); 10 } 11 return df; 12 } 13 14 public static String formatDate(Date date) throws ParseException { 15 return getDateFormat().format(date); 16 } 17 18 public static Date parse(String strDate) throws ParseException { 19 return getDateFormat().parse(strDate); 20 } 21 }
5.测试和结论
做一个简单的压力测试,方法一最慢,方法三最快。
但是就算是最慢的方法一性能也不差,一般系统方法一和方法二就可以满足,所以说在这个点很难成为系统的瓶颈所在。从简单的角度来说,建议使用方法一或者方法二,如果在必要的时候,追求那么一点性能提升的话,可以考虑用方法三,用ThreadLocal做缓存。
ps:Joda-Time类库对时间处理方式比较完美,建议使用。(待学习)