SimpleDateFormat线程安全问题与解决办法

SimpleDateFormat线程安全问题与解决办法

薛定谔的汪

今天在自己的项目中遇到了SimpleDateFormat线程安全的问题,参考其他项目的DateUtil里把SimpleDateFormat都声明为

1
public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

但是这样存在一个线程完全的问题,原因是因为 DateFormat 和 SimpleDateFormat 类不都是线程安全的,在多线程环境下调用 format() 和 parse() 方法可能会获得我们意料之外的结果,甚至抛出异常。
在程序中我们应当尽量少的创建SimpleDateFormat 实例,因为创建这么一个实例需要耗费很大的代价,然后再丢弃这个对象。大量的对象就这样被创建出来,占用相当一部分的内存和 JVM空间。

SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用,这样多线程操作同一个sdf对象时就会引发一些问题。 JDK原始文档如下:
  Synchronization:
  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.
SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。

用jmeter工具测试,在50个线程下,每个线程连续请求五次,这个静态的sdf就会报java.lang.NumberFormatException异常。

解决办法

在需要的方法里创建新的实例:

在需要用到SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。

1
2
3
4
5
6
7
8
9
10
11
public class DateUtil {	
public static String formatDate(Date date)throws ParseException{
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
return sdf.format(date);
}

public static Date parse(String strDate) throws ParseException{
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
return sdf.parse(strDate);
}
}

使用同步:同步SimpleDateFormat对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DateSyncUtil {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date)throws ParseException{
synchronized(sdf){
return sdf.format(date);
}
}

public static Date parse(String strDate) throws ParseException{
synchronized(sdf){
return sdf.parse(strDate);
}
}
}

当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。

绑定当前线程ThreadLocal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DateUtil {
private static final String date_format = "yyyyMMdd";
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();

public static DateFormat getDateFormat(){
DateFormat df = threadLocal.get();
if(df==null){
df = new SimpleDateFormat(date_format);
threadLocal.set(df);
}
return df;
}

public static String formatDate(Date date) throws ParseException {
return getDateFormat().format(date);
}

public static Date parse(String strDate) throws ParseException {
return getDateFormat().parse(strDate);
}
}

或者可以这样,重写initialValue()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DateUtil {
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
};

public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}

public static String format(Date date) {
return threadLocal.get().format(date);
}
}

注:initialValue()方法:返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次。如果希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法

使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。

使用其他类库中的时间格式化类:

1,使用Apache commons lang3包下的FastDateFormat,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行format, 对日期字符串解析比较局限。

2,使用Joda-Time类库来处理时间相关问题

  • Title: SimpleDateFormat线程安全问题与解决办法
  • Author: 薛定谔的汪
  • Created at : 2017-10-31 17:15:14
  • Updated at : 2023-11-17 19:37:37
  • Link: https://www.zhengyk.cn/2017/10/31/java/Thread_sdf/
  • License: This work is licensed under CC BY-NC-SA 4.0.
On this page
SimpleDateFormat线程安全问题与解决办法