SimpleDateFomat线程安全问题

​        在多线程情况下使用SimpleDateFormat要尤其注意,它不是线程安全的。问题是一个老问题,但是经常出现。

        在java的api中这样描述:

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

        这个类一个典型的场景是按照指定的格式,将日期类型数据转换为字符串类型,或者将字符串类型转换为日期类型。

(1)多线程场景,在方法体内部new DateFormat对象,但是这种情况对于内存开销比较大,同时对于一些可以复用的格式,重复来拷贝代码看起来有点恶心;

(2)通过synchronize机制,但是线程同步同样会造成性能下降;

(3)到通过静态工厂方法例如“getDateInstance”来创建DateFormat的对象,这种方法看起来很有效,但是由于在DateFormat中缺乏同步,所以在多线程获取同一个对象的时候会出现问题,典型的异常有NumberFormatExcetion和ArrayIndexOutOfBoundsException;

(4)通过ThreadLocal来搞定;

(5)使用第三方工具包,例如apache-common的DateFormatUtils;

 

A、首先模拟一个线程安全问题

在用到format的时候,我们经常在方法中new一个对象,SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");但是有时候,我们为了省事和复用,把这个对象作为静态对象来抽取出来,但是这样是有问题的,线程不安全。

下面这个例子,SimpleDateFormat的对象是静态类,所以执行结果的时候就出现错乱的情况了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import  java.text.SimpleDateFormat;
import  java.util.Date;
import  java.util.concurrent.ExecutorService;
import  java.util.concurrent.Executors;
                     
import  org.junit.Test;
public  class  SimpleTest {
     public  static  void  main(String[] args) {
         ExecutorService threadPool = Executors.newFixedThreadPool( 10 );
             for ( int  i= 0 ;i< 30 ;i++){
                 threadPool.submit( new  TestSimpleThread( new  Date( 2012 , 11 ,i),i));
             }
     }
}
class  TestSimpleThread  implements  Runnable{
     private  static  SimpleDateFormat sf =  new  SimpleDateFormat( "yyyy-MM-dd" );
                     
     private  Date date;
     private  int  temp;
     public  TestSimpleThread(Date d, int  t){
         date = d;
         temp = t;
     }
     @Override
     public  void  run() {
         System.out.println(Thread.currentThread().getName()+ "---" +temp+ "---" +sf.format(date));
     }
                     
}

按照我们的设计,两列值如果线程安全,那应该是相同的,但是执行的时候出现了错误。

 

B、上面SimpleDateFormat是类的静态属性,通过加锁能够解决这个问题,下面例子跑出来的结果是正常的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import  java.text.SimpleDateFormat;
import  java.util.Date;
import  java.util.concurrent.ExecutorService;
import  java.util.concurrent.Executors;
               
import  org.junit.Test;
public  class  SimpleTest {
     public  static  void  main(String[] args) {
         ExecutorService threadPool = Executors.newFixedThreadPool( 10 );
             for ( int  i= 0 ;i< 30 ;i++){
                 threadPool.submit( new  TestSimpleThread( new  Date( 2012 , 11 ,i),i));
             }
     }
}
class  TestSimpleThread  implements  Runnable{
     private  static  SimpleDateFormat sf =  new  SimpleDateFormat( "yyyy-MM-dd" );
               
     public  String convertDateToString(Date d)  throws  Exception {
         String result;
         synchronized  (sf) {
             result = sf.format(d);
         }
         return  result;
     }
               
     private  Date date;
     private  int  temp;
     public  TestSimpleThread(Date d, int  t){
         date = d;
         temp = t;
     }
     @Override
     public  void  run() {
         try  {
             System.out.println(Thread.currentThread().getName()+ "---" +temp+ "---" +convertDateToString(date));
         catch  (Exception e) {
             //
         }
     }
               
}

在进行format的时候加了同步:

C、通过 SimpleDateFormat.getDateInstance()来获取对象进行操作,没有还原上面说的场景,但是发现一个问题,就是里面的参数是int类型,如果想灵活的进行自定义格式,用起来不是很方便。

D、采用ThreadLocal的方式来解决线程安全问题

下面这个是通过覆盖initialValue方法,

1
2
3
4
5
6
7
8
9
10
11
12
public  class  ThreadLocalSF {
        
     private  static  ThreadLocal<DateFormat> threadDateFormatDefault =  new  ThreadLocal<DateFormat>(){
         @Override
         protected  DateFormat initialValue() {
             return  new  SimpleDateFormat( "yyyy-MM-dd" );
         }
     };
     public  static  String convertDateToString(Date d) {
         return  threadDateFormatDefault.get().format(d);
     }
}

还有一中就是 获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DateUtil {
  
     private static final String DATE_FORMAT =  "yyyy-MM-dd HH:mm:ss" ;
  
     // 第一次调用get将返回null
     private static ThreadLocal threadLocal =  new  ThreadLocal();
  
     // 获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中
     public static DateFormat getDateFormat() {
         DateFormat df = (DateFormat) threadLocal.get();
         if  (df ==  null ) {
             df =  new  SimpleDateFormat(DATE_FORMAT);
             threadLocal.set(df);
         }
         return  df;
     }
  
}

 

网上有张图对于三种方式的TPS做了对比,性能最好的是ThreadLocal的方式。

 

看了一些公共的DateUtil类,基本上采用的最简单的模式:

写一个静态类DateUtil,里面搞一个静态方法,每个方法内部new一个SimpleDateFormat的类,然后进行调用,这种最简单呵呵。

 

参考:http://www.javacodegeeks.com/2010/07/java-best-practices-dateformat-in.html

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(simple)