SimpleDateFormat Thread Security

Keywords: Programming Java Lambda

background

As we all know, SimpleDateFormat in Java is not thread-safe and can cause unexpected problems under multiple threads.This article will explore the specific reasons why SimpleDateFormat threads are insecure to gain a deeper understanding of thread security.

Example

Simple test code that can cause problems when multiple threads call the parse method at the same time:

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

Part of the output is as follows:

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)

Not surprisingly, every run will make a mistake, and occasionally the output initial time Thu Jan 01 00:00:00 GMT 1970 and other unexpected times will occur.Okay, keep these two errors in mind. Let's analyze them carefully below.

Analysis

SimpleDateFormat inherits from the abstract class DateFormat, and the UML diagram is as follows:

There are two global variables in DateFormat that need attention

public abstract class DateFormat extends Format {

    //Calendar variable, as a supplement to DateFormat
    protected Calendar calendar;

    //Used for Format numbers, defaulting to DecimalFormat
    protected NumberFormat numberFormat;
}

public class DecimalFormat extends NumberFormat {
    //Global variables in DecimalFormat to store converted data
    //The digitList is represented by scientific and technological counts, such as 2019 as 0.2019x10^4
    private transient DigitList digitList = new DigitList();
}

Initialization of these two variables is initialized in the construction method of SimeDateFormat. Looking at the class structure, let's take a closer look at DateFormat's parse method and go directly to the code (omitting some trivial code):

public Date parse(String text, ParsePosition pos)
{
    ......
    //Notice the variable calb, the date conversion is done through the CalendarBuilder class
    CalendarBuilder calb = new CalendarBuilder();

    //Follow DateFormat's pattern one by one (year, month, day, hour, second...)
    for (int i = 0; i < compiledPattern.length; ) {
        ......
        //Final call to subParse method to assign calb
        start = subParse(text, start, tag, count, obeyCount, ambiguousYear, pos, useFollowingMinusSignAsDelimiter, calb);
    }
    Date parsedDate;
    try {
        //Call CalendarBuilder's establish method to pass the value to the variable calendar
        //Get the date of final return through calendar
        //Notice that calendar is a global variable here
        parsedDate = calb.establish(calendar).getTime();
    }
    ......

    return parsedDate;
}

The main steps are as follows: > 1. Define a CalendarBuilder object calb to temporarily save parse results. > 2. According to the Pattern defined by DateFormat, the for loop calls the subParse method, converting the target strings one by one (year, month, day, time, second...) and storing them in the calb variable. > 3. Call the calb.establish(calendar) method to set the data temporarily stored in the CALB to the global variable calendar. > 4. calendar now contains converted date data, and finally calls the **Calendar.getTime()** method to return the date.

One of the problems

Let's look at what's done inside the subParse method and what's wrong with the implementation.Look at the code first (omitted some unimportant code):

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) {
        //Some variable initialization
        ......

        //Internally invoke the parse method of numberFormat to convert numbers
        //Here, numberFormat is the global variable analyzed above, and the default instance is DecimalFormat
        //text is the substitution string "2019/11/11 11:11:11", pos is the location, if 2019 will be converted to 0.2019x10^4
        number = numberFormat.parse(text, pos);
        }
        if (number != null) {
            //Converting to an int value, such as 0.2019x10^4, will convert to 2019
            value = number.intValue();
        }
        int index;
        switch (patternCharIndex) {
        case PATTERN_YEAR:      // 'y'
            //Years, months, days, and so on. Here's just an example of PATTERN_YEAR
            //set the value from numberFormat parse into calb
            calb.set(field, value);
            return pos.index;
        }

        ......

        // Escape Failure
        origPos.errorIndex = pos.index;
        return -1;
    }
}

//numberFormat.parse(text, pos) method implementation
public class DecimalFormat extends NumberFormat {

    public Number parse(String text, ParsePosition pos) {{
        //Call the subparse method internally to set the contents of the text onto the digitList
        if (!subparse(text, pos, positivePrefix, negativePrefix, digitList, false, status)) {
            return null;
        }
        ......

        //Convert digitList to target format
        if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {
            //parse is of Long type
            longResult = digitList.getLong();
        } else {
            //parse is 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[]) {
        //Some judgments and variable initialization preparation
        ......

        //In this method, digitList is called digits, which is cleared first.
        //DecimalAts are decimal places, such as 0.2019x10^4 where decimalAt s are 4
        //The number of digits in the count index, such as count of 0.2019x10^4, is 4
        digits.decimalAt = digits.count = 0;

        backup = -1;
        for (; position < text.length(); ++position) {
            //An assignment to digits within a loop sets variables for each part of scientific counting
            //Notice that this digits is a global variable
            ......
        }

        //Also continue with digits
        if (!sawDecimal) {
            digits.decimalAt = digitCount; // Not digits.count!
        }
        digits.decimalAt += exponent;

        ......
        return true;
    }
}

Seeing this, students with some concurrent programming experience can probably see the problem.Unprotected within the subparse method, this variable is likely to be an invalid value when multiple threads operate on the global variable digits(digitList) simultaneously.For example, thread A sets the value in half, and another thread B initializes the value in zero again.Thread A then either obtains an unexpected value or directly errors NumberFormatException later in digitList.getDouble() and digitList.getLong().

Question Two

So are there any questions about the next steps?Keep looking down. Previously, the method first places the parse good value in the CalendarBuilder type temporary variable calb, then calls the establish method to set the cached value in the calendar variable in SimpleDateFormat. Let's look at the establish method below:

class CalendarBuilder {
    Calendar establish(Calendar cal) {
        ......
        //This cal is the member variable calendar in SimpleDateFormat
        //Initialize data cleanup in cal s, the same routine as digitList above
        cal.clear();
        
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    //The previous temporary CalendarBuild values are placed in the field array.
                    //Here the values in the array are assigned to cal one by one
                    cal.set(index, field[MAX_FIELD + index]);
                    break;
                }
            }
        }

        if (weekDate) {
            //Set the weekdate field of cal
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
        }
        return cal;
    }
}

Again, since **calendar(cal) is a global variable, thread security issues occur when multiple threads call the establish method simultaneously.For a simple example, thread A was originally assigned "2019/11/11 11:11:11:11", so thread B called cal.clear()** and cleaned up the data again, so thread A returned to the pre-release date and output the date "1970/01/01 00:00:00".

Solution

For thread-safe solutions, adding synchronization to a method is the simplest, equivalent to a thread accessing a parse method one at a time:

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

The more common posture, of course, is to work with ThreadLocal, which is equivalent to defining a format variable for each thread without interacting with each other:

    private ThreadLocal<simpledateformat> format = new ThreadLocal<simpledateformat>(){  
        [@Override](https://my.oschina.net/u/1162528)  
        protected SimpleDateFormat initialValue() {  
            return new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");  
        }  
    };

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

However, it is recommended that you not use SimpleDateFormat, but use the new class LocalDateTime or DateTimeFormatter introduced in Java8, which is not only thread safe but also more efficient.

summary

This article examines the reasons why SimpleDateFormat threads are insecure at the code level.Both subparse and establish methods can cause problems, and the former throws an Exception. In summary, the problem lies in global variables.So be careful when defining global variables, and be aware that variables are thread safe.</simpledateformat></simpledateformat>

Posted by Deany on Sun, 08 Dec 2019 08:22:33 -0800