2017.12.11SimpleDateFormat的线程安全性讨论

转载来自:http://blog.csdn.net/zxh87/article/details/19414885java

 

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的说明:app

SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每一个线程建立独立的格式实例。若是多个线程同时访问一个格式,则它必须保持外部同步。ide

1 Synchronization2   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类库对时间处理方式比较完美,建议使用。(待学习)

相关文章
相关标签/搜索