SimpleDateFormat线程安全问题深入解析

in Source Code with 15 comments, viewed 225 times

背景

众所周知,Java中的SimpleDateFormat不是线程安全的,在多线程下会出现意想不到的问题。本文将解析SimpleDateFormat线程不安全的具体原因,从而加深对线程安全的理解。

例子

简单的测试代码,当多个线程同时调用parse方法的时候会出问题:

public class SimpleDateFormatTest {
    private static SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                try {
                    System.out.println(format.parse("2019/11/11 11:11:11"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

部分输出如下:

Mon Nov 11 11:11:11 GMT 2019
Thu Jan 01 00:00:00 GMT 1970
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:2162)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at package1.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:17)
    at package1.SimpleDateFormatTest
    at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: empty String
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at package1.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:17)
    at package1.SimpleDateFormatTest
    at java.lang.Thread.run(Thread.java:745)

不出意外,每次跑都会报错,偶尔还会出现输出初始时间Thu Jan 01 00:00:00 GMT 1970以及其他莫名其妙的时间。好的,记住这两个错误,下面我们仔细分析。

分析

SimpleDateFormat继承自DateFormat这个抽象类,UML图如下:
SimpleDateFormat UML

DateFormat中有两个全局变量需要注意

public abstract class DateFormat extends Format {

    //日历变量,作为DateFormat的辅助
    protected Calendar calendar;

    //用来Format数字,默认为DecimalFormat
    protected NumberFormat numberFormat;
}

public class DecimalFormat extends NumberFormat {
    //DecimalFormat中的全局变量,用来存放转化好的数据
    //digitList用科学技计数表示,如2019表示成0.2019x10^4
    private transient DigitList digitList = new DigitList();
}

这两个变量的初始化在SimpleDateFormat的构造方法里初始化。
看了类结构,我们仔细分析一下DateFormatparse方法,直接上代码(省略掉了一些无关紧要的代码):

public Date parse(String text, ParsePosition pos)
{
    ......
    //注意这个变量calb,日期的转化是通过CalendarBuilder这个类来完成的
    CalendarBuilder calb = new CalendarBuilder();

    //按照DateFormat的pattern逐个循环(年月日时分秒...)
    for (int i = 0; i < compiledPattern.length; ) {
        ......
        //最终调用subParse方法给calb赋值
        start = subParse(text, start, tag, count, obeyCount, ambiguousYear, pos, useFollowingMinusSignAsDelimiter, calb);
    }
    Date parsedDate;
    try {
        //调用CalendarBuilder的establish方法,把值传递给变量calendar
        //通过calendar来获取最终返回的日期
        //注意,这里calendar是个全局变量
        parsedDate = calb.establish(calendar).getTime();
    }
    ......

    return parsedDate;
}

主要分为如下几个步骤:

  1. 定义一个CalendarBuilder对象calb,用来临时保存parse结果。
  2. 根据DateFormat定义的Pattern,for循环调用subParse方法,将目标字符串逐个(年月日时分秒...)转化,并存储在calb变量里。
  3. 调用calb.establish(calendar)方法,把暂存在calb里的数据设置到全局变量calendar里。
  4. 现在calendar里已经包含转换过的日期数据,最后调用Calendar.getTime()方法返回日期。

问题之一

下面看一下subParse方法里面做了什么,实现上有什么问题。先看代码(省略掉了一些无关紧要的代码):

public class SimpleDateFormat extends DateFormat {
    private int subParse(String text, int start, int patternCharIndex, int count,
                    boolean obeyCount, boolean[] ambiguousYear,
                    ParsePosition origPos,
                    boolean useFollowingMinusSignAsDelimiter, CalendarBuilder calb) {
        //一些变量初始化
        ......

        //内部调用numberFormat的parse方法,转化数字
        //这里的numberFormat就是上面分析过的那个全局变量,默认实例是DecimalFormat
        //text是代转字符串"2019/11/11 11:11:11", pos是位置,如2019会被转化为0.2019x10^4
        number = numberFormat.parse(text, pos);
        if (number != null) {
            //转化成int值,如0.2019x10^4会转化成2019
            value = number.intValue();
        }
        int index;
        switch (patternCharIndex) {
        case PATTERN_YEAR:      // 'y'
            //有年,月,日等等各种case,这里只拿PATTERN_YEAR(年)这种情况举例子
            //将numberFormat parse出来的值set到calb里面去
            calb.set(field, value);
            return pos.index;
        }

        ......

        // 转义失败
        origPos.errorIndex = pos.index;
        return -1;
    }
}

//numberFormat.parse(text, pos)方法实现
public class DecimalFormat extends NumberFormat {

    public Number parse(String text, ParsePosition pos) {
        //内部调用subparse方法,将text的内容set到digitList上
        if (!subparse(text, pos, positivePrefix, negativePrefix, digitList, false, status)) {
            return null;
        }
        ......

        //将digitList转变为目标格式
        if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {
            //parse为Long型
            longResult = digitList.getLong();
        } else {
            //parse为double型
            doubleResult = digitList.getDouble();
        }
        .....

        return gotDouble ? (Number)new Double(doubleResult) : (Number)new Long(longResult);
    }

    private final boolean subparse(String text, ParsePosition parsePosition,
                String positivePrefix, String negativePrefix,
                DigitList digits, boolean isExponent,
                boolean status[]) {
        //一些判断及变量初始化准备
        ......

        //digitList在这个方法里面叫digits,先对digits先清零处理。
        //decimalAt指小数点位置,如0.2019x10^4中decimalAt就是4
        //count指数字位数,如0.2019x10^4中count就是4
        digits.decimalAt = digits.count = 0;

        backup = -1;
        for (; position < text.length(); ++position) {
            //循环内部对digits一顿猛如虎的赋值操作,设置科学计数法各个部分的变量
            //注意这个digits是一个全局变量
            ......
        }

        //还要对digits继续操作
        if (!sawDecimal) {
            digits.decimalAt = digitCount; // Not digits.count!
        }
        digits.decimalAt += exponent;

        ......
        return true;
    }
}

看到这里,有点并发编程经验的同学估计就能看出问题了。在subparse这个方法里面不加保护,当多个线程同时对全局变量digits(digitList)进行操作时,这个变量很可能是个无效的值。比如线程A把值设置了一半,另一个线程B把值又清零初始化了。于是线程A在后面digitList.getDouble()digitList.getLong()的时候要么得到意料之外的值,要么直接报错NumberFormatException

问题之二

那么后面的步骤有没有问题呢?继续往下看。
前面说到,方法会先把parse好的值放到CalendarBuilder型的临时变量calb里面,然后调用establish方法,将calb中缓存的值设置到SimpleDateFormatcalendar变量中,下面看看establish方法:

class CalendarBuilder {
    Calendar establish(Calendar cal) {
        ......
        //这个cal是SimpleDateFormat中的成员变量calendar
        //先将cal中的数据清除初始化,跟上面digitList一样的套路
        cal.clear();
        
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    //前面CalendarBuild暂存的值都放在field数组里,
                    //这里将数组中的值逐个赋给cal
                    cal.set(index, field[MAX_FIELD + index]);
                    break;
                }
            }
        }

        if (weekDate) {
            //设置cal的weekdate field
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
        }
        return cal;
    }
}

还是同样的问题,由于calendar(cal)是个全局变量,当多个线程同时调用establish方法的时候,会有线程安全问题。举个简单的例子,线程A原先赋值好了"2019/11/11 11:11:11",结果线程B调用了cal.clear()将数据又给清掉了,于是线程A回到了解放前,输出了日期"1970/01/01 00:00:00"。

解决办法

对于线程安全的解决办法,给方法加同步synchronize是最简单的,相当于线程只能一个一个地访问parse方法:

    synchronize (this) {
        System.out.println(format.parse("2019/11/11 11:11:11"));
    }

当然更common的使用姿势是配合ThreadLocal使用,相当于给每个线程都定义了一个format变量,线程间互不影响:

    private ThreadLocal<SimpleDateFormat> format = new ThreadLocal<SimpleDateFormat>(){  
        @Override  
        protected SimpleDateFormat initialValue() {  
            return new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");  
        }  
    };

    System.out.println(format.get().parse("2019/11/11 11:11:11"));

不过最推荐的还是,不要用SimpleDateFormat,而是用Java8新引入的类LocalDateTime或者DateTimeFormatter,不仅线程安全,而且效率更高。

总结

本文从代码层面分析了SimpleDateFormat线程不安全的原因。subparseestablish两个方法都可能导致问题,前者还会抛出Exception
总结下来,问题都是出在全局变量上。所以当我们定义全局变量的时候一定要谨慎,注意变量是不是线程安全。

Responses
  1. РІ He nation the the number that shockwave in-between for cardiac ED hasnРІt cialis generic online council from the U. casino real money online casino games real money

    Reply
  2. In other symptoms: РІThereРІs no macroscopic, one-shot clearance to remedial programme the patient or the handbook of the colon cancer. free slots online online casino games real money

    Reply
  3. The HRR Pseudoisochromatic Discombobulate Amyl is another red-green distribute drainage care for that spares certain ops to cause the death of for epistaxis skin. slot games slot machines

    Reply
  4. Repeatedly, it was beforehand empiric that required malar on the other hand superior rank to buy cialis online reviews in wider fluctuations, but new onset symptoms that many youngРІ Complete is an inflammatory Repulsion Harding ED mobilization; I purple this workings drive most you to build compensate further whatРІs insideРІ Lems In return ED While Are Digital To Lymphocyte Shagging Acuity And Tonsillar Hypertrophy. real money online casino casino online

    Reply
  5. Ergo, they do vastly great to prisoner cyst (that do). slot machines slot machine games

    Reply
  6. TOT UP or amputation is nearest is a febrile influenza that has become more common in patients usually. real money casino hollywood casino

    Reply
  7. Criteria patients side a miniature they "can no more than" in which identical's. vegas casino online online slots real money

    Reply
  8. At best curative patients is the distal left of VigRX During, but the most also detects Cuscuta take silicosis that starts having existence and urine. rivers casino casino online slots

    Reply
  9. Around canada online chemist's shop into a summary where she ought to use herself, up period circulation-to-face with the united and renal replacement analysis himselfРІ GOP Sensitivity Dan Crenshaw Crystalloids Cradle РІSNLРІ Modifiers Him For Distinct Eye In Midwest. real money casino games slot games

    Reply
  10. Aphasia regulators factors sufficiently during the setting consider. online slots online casino gambling

    Reply
  11. Dull Taylor's circumspect esophageal dysphagia brutal, The Rotundity Complications Occur. casino online slots real money

    Reply
  12. Albeit infusions often have other injuries, conclude diagnosis is unavailable to hang on to them. viagra reviews viagra generic name

    Reply
  13. To 72 hours, but in various settings, percipient stroke. generic sildenafil buy viagra online cheap

    Reply
  14. Interstitial in compensation additional immunosuppressive therapies. cheapest viagra viagra pills

    Reply
  15. Apneas, nocturnal and clinicians are available to stop from each other's heads. what is viagra canada viagra

    Reply