SimpleDateFormat并发隐患及其解决

来源:https://blog.csdn.net/paincupid/article/details/77531362

SimpleDateFormat被大量使用于处理时间格式化过程,但时间转换过程遇到的多线程并发使用场景并不多见,这其中存在严重并发隐患,本文就此问题展开分析并介绍几种可行的解决方案。

SimpleDateFormat被大量使用于处理时间格式化过程,由于该类在创建时会指定一个pattern用于标明固定的时间格式,所以在使用中,一般会创建一个作用域较大(static修饰或某类的私有属性)的对象用于重复使用。由于时间转换过程遇到的多线程并发的使用场景并不多见,所以很难发现在该类的隐患,事实上,该类并非是线程安全的,在多线程使用format()和parse()方法时可能会遇到问题。

分析

在SimpleDateFormat及其父类DateFormat的源文件里,有这样一段说明:

* 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.

JDK文档中已经明确指出,这两个类在进行时间格式化的过程中都是非线程安全的。也就是说,使用同一个SimpleDateFormat实例,开若干线程做日期转换操作,得到的结果可能并不准确。

parse

parse()测试,参考了其他人对此做的实验,我使用的测试代码(jdk1.8)如下:

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class DateFormatTest extends Thread {

private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

private String name;

private String dateStr;

public DateFormatTest(String name, String dateStr) {

this.name = name;

this.dateStr = dateStr;

}

@Override

public void run() {

Date date = null;

try {

date = sdf.parse(dateStr);

} catch (ParseException e) {

e.printStackTrace();

}

System.out.println(name + " : date: " + date);

}

public static void main(String[] args) throws InterruptedException {

ExecutorService executor = Executors.newCachedThreadPool();

executor.execute(new DateFormatTest("Test_A", "2000-04-28"));

executor.execute(new DateFormatTest("Test_B", "2017-04-28"));

executor.shutdown();

}

}

这段测试代码参考了网上一段用例,与之不同的是,原用例中在两个线程操作中间做了线程等待Sleep,而为了看到效果,修改后的测试用例把线程等待的部分去掉。虽然每次运行的结果都会不太一样,但经常会抛出的异常:

Exception in thread "pool-1-thread-1"java.lang.NumberFormatException: For input string: ""

at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)

at java.lang.Long.parseLong(Long.java:601)

at java.lang.Long.parseLong(Long.java:631)

at java.text.DigitList.getLong(DigitList.java:195)

at java.text.DecimalFormat.parse(DecimalFormat.java:2051)

at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)

at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)

at java.text.DateFormat.parse(DateFormat.java:364)

at DateFormatTest.run(DateFormatTest.java:24)

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)

at java.lang.Thread.run(Thread.java:745)

Test_B : date: Mon Apr 28 00:00:00 CST 2200


需要明确的是,待转换的字符串作为非静态私有变量是每个对象持有的,只有sdf本身是公用的,不难发现即便是成功输出了,但是数值也未必会是正确的,parse()方法不安全。

format

SimpleDateFormat的format()方法源码如下:

private StringBuffer format(Date date, StringBuffer toAppendTo,

FieldDelegate delegate) {

// Convert input date to time field list

calendar.setTime(date);

...


需要注意的是calendar的操作并非是线程安全的,很显然在并发情景下,format的使用并不安全,测试过程与对parse过程的测试相似,不再赘述。

解决

既然SimpleDateFormat本身并不安全,那么解决的方式无非两种:优化使用过程或者找替代品。

1.临时创建

不使用Static,每次使用时,创建新实例。

存在的问题:

SimpleDateFormat中使用了Calendar对象,由于该对象相当重,在高并发的情况下会大量的new SimpleDateFormat以及销毁SimpleDateFormat,极其耗费资源。

2.synchronized

以synchronized同步SimpleDateFormat对象。

存在的问题:

高并发时,使用该对象会出现阻塞,当前使用者使用时,其他使用者等待,尽管结果是对的,但是并发成了排队,实际上并没有解决问题,还会对性能以及效率造成影响。

3.ThreadLocal

使用ThreadLocal,令每个线程创建一个当前线程的SimpleDateFormat的实例对象。

public class ConcurrentDateUtil {

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {

        @Override

        protected DateFormat initialValue() {

            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        }

    };



    public static Date parse(String dateStr) throws ParseException {

        return threadLocal.get().parse(dateStr);

    }



    public static String format(Date date) {

        return threadLocal.get().format(date);

    }

}

或

public class ThreadLocalDateUtil {

    private static final String date_format = "yyyy-MM-dd HH:mm:ss";

    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);

    }   

}

存在的问题:

使用ThreadLocal时,如果执行原子任务的过程是每一个线程执行一个任务,那么这样的声明基本和每次使用前创建实例对象是没区别的;如果使用的是多线程加任务队列,举个例子,tomcat有m个处理线程,外部有n个待处理任务请求,那么当执行n个任务时,其实只会创建m个SimpleDateFormat实例,对于单一的处理线程,执行任务是有序的,所以对于当前线程而言,不存在并发。

4.Apache的 DateFormatUtils 与 FastDateFormat

使用org.apache.commons.lang.time.FastDateFormat 与 org.apache.commons.lang.time.DateFormatUtils。

存在的问题:

apache保证是线程安全的,并且更高效。但是DateFormatUtils与FastDateFormat这两个类中只有format()方法,所有的format方法只接受long,Date,Calendar类型的输入,转换成时间串,目前不存在parse()方法,可由时间字符串转换为时间对象。

5.Joda-Time

使用Joda-Time类库。

存在的问题:

没有问题~

简介:

Joda-Time — 面向 Java 应用程序的日期/时间库的替代选择,Joda-Time 令时间和日期值变得易于管理、操作和理解。事实上,易于使用是 Joda 的主要设计目标。其他目标包括可扩展性、完整的特性集以及对多种日历系统的支持。并且 Joda 与 JDK 是百分之百可互操作的,因此您无需替换所有 Java 代码,只需要替换执行日期/时间计算的那部分代码。

资料:

Joda-Time 简介(中文)https://www.ibm.com/developerworks/cn/java/j-jodatime.html
Joda-Time 文档(英文)http://joda-time.sourceforge.net/

猜你喜欢

转载自blog.csdn.net/HonsonNgai/article/details/85273353