zoukankan      html  css  js  c++  java
  • 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的实例对象。


    存在的问题:

    使用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/




    “欲要看究竟,处处细留心。”  —宋帆


    免费体验云安全(易盾)内容安全、验证码等服务

    更多网易技术、产品、运营经验分享请点击


    相关文章:
    【推荐】 网易云数据库架构设计实践
    【推荐】 jq一个强悍的json格式化查看工具

  • 相关阅读:
    POJ 2251 Dungeon Master
    HDU 3085 Nightmare Ⅱ
    CodeForces 1060 B Maximum Sum of Digits
    HDU 1166 敌兵布阵(树状数组)
    HDOJ 2050 折线分割平面
    HDU 5879 Cure
    HDU 1878 欧拉回路
    HDU 6225 Little Boxes
    ZOJ 2971 Give Me the Number
    HDU 2680 Choose the best route
  • 原文地址:https://www.cnblogs.com/163yun/p/9876687.html
Copyright © 2011-2022 走看看