preface

In the process of project development, I suddenly encountered a problem. The storage time in the database was originally 2018.03.21 08:08:08. The data stored later changed to 2018.03.21 08:08:08.08. Result The following error message is displayed when a number is fetched from the database:During troubleshooting, the time was found to have been transferred from the workflow. Here’s how to safely format SimpleDateFormat time in Java.

SimpleDateFormat is no stranger to you. SimpleDateFormat is a very common class in Java that is used to parse and format date strings, but can lead to very subtle and difficult debugging problems if not used carefully, because neither the DateFormat nor SimpleDateFormat classes are thread-safe. Calling the Format () and parse() methods in multithreaded environments should use synchronized code to avoid thread insecurity. Let’s take a step-by-step look at the SimpleDateFormat class through a concrete scenario.

A tease.

You should create as few instances of SimpleDateFormat as possible in your program, because creating such an instance is expensive. In an example of reading database data and exporting it to an Excel file, a SimpleDateFormat instance object needs to be created and then discarded each time a time message is processed. A large number of objects are created in this way, consuming a lot of memory and JVM space. The code is as follows:

package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { public static String formatDate(Date date)throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } public static Date parse(String strDate) throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(strDate); }}Copy the code

You might say, OK, I’ll just create a static instance of simpleDateFormat, put it in a DateUtil class (below), and use that instance when I use it, and that’ll solve the problem. The improved code looks like this:

package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDate(Date date)throws ParseException{ return sdf.format(date); } public static Date parse(String strDate) throws ParseException{ return sdf.parse(strDate); }}Copy the code

Of course, this method works really well and works well most of the time. But when you use it in a production environment for a while, you realize that it’s not thread-safe. Under normal testing conditions, there was no problem, but once under certain load conditions in a production environment, the problem arose. There are all kinds of things that can happen, such as incorrect conversion time, such as an error, such as thread hanging, and so on. Let’s look at the following test case and let the facts speak for themselves:

package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDate(Date date)throws ParseException{ return sdf.format(date); } public static Date parse(String strDate) throws ParseException{ return sdf.parse(strDate); } } package com.peidasoft.dateformat; import java.text.ParseException; import java.util.Date; public class DateUtilTest { public static class TestSimpleDateFormatThreadSafe extends Thread { @Override public void run() { while(true) { try { this.join(2000); } catch (InterruptedException e1) { e1.printStackTrace(); } try { System.out.println(this.getName()+":"+DateUtil.parse("2013-05-24 06:02:20")); } catch (ParseException e) { e.printStackTrace(); } } } } public static void main(String[] args) { for(int i = 0; i < 3; i++){ new TestSimpleDateFormatThreadSafe().start(); }}}Copy the code

The output is as follows:

Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
    at java.text.DateFormat.parse(DateFormat.java:335)
    at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
    at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
    at java.text.DateFormat.parse(DateFormat.java:335)
    at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
    at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Thread-2:Mon May 24 06:02:20 CST 2021
Thread-2:Fri May 24 06:02:20 CST 2013
Thread-2:Fri May 24 06:02:20 CST 2013
Thread-2:Fri May 24 06:02:20 CST 2013
Copy the code

Description: Thread 1 and Thread – 0 to Java. Lang. A NumberFormatException: multiple points error, direct hang dead, no; Although Thread-2 did not die, the output time was incorrect. For example, the input time was 2013-05-24 06:02:20, but the output time was “Mon May 24 06:02:20 CST 2021”.

Ii. Problem analysis

As professional programmers, we all know, of course, that the cost of sharing a variable is much less than creating a new variable each time. The static SimpleDateFormat optimized above has all sorts of weird errors in concurrency because SimpleDateFormat and DateFormat classes are not thread-safe. We ignore thread-safety because it’s hard to see what thread-safety has to do with the SimpleDateFormat and DateFormat classes that provide us with interfaces. At the bottom of the JDK documentation is the following:

  • The date format in SimpleDateFormat is not synchronous. It is recommended to create a separate instance of the format for each thread. If multiple threads access a format at the same time, it must maintain external synchronization.

The original JDK documentation is as follows:

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

Let’s take a look at the JDK source code to see why the SimpleDateFormat and DateFormat classes aren’t really thread-safe:

SimpleDateFormat inherits DateFormat and defines a protected Calendar class object in DateFormat: Calendar. Just because the Calendar class is complex in concept, involving time zones, localization, etc., the JDK implementation uses member variables to pass parameters, which can cause errors in multi-threading. In the format method, there is this code:

private StringBuffer format(Date date, StringBuffer toAppendTo,
        FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);
        boolean useDateFormatSymbols = useDateFormatSymbols();
        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
              count = compiledPattern[i++] << 16;
              count |= compiledPattern[i++];
        }

        switch (tag) {
          case TAG_QUOTE_ASCII_CHAR:
                   toAppendTo.append((char)count);
                   break;

         case TAG_QUOTE_CHARS:
                   toAppendTo.append(compiledPattern, i, count);
                   i += count;
                   break;
         default:
            subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
        break;
        }
    }
        return toAppendTo;
    }
Copy the code

The calendar.settime (date) statement changes the calendar, which will be used later (in the subFormat method), and this is the root cause of the problem. Imagine a multithreaded environment where two threads hold the same instance of SimpleDateFormat and call the format method:

  1. Thread 1 calls the format method, changing the Calendar field. The interruption came.
  2. Thread 2 starts executing, which also changes the calendar. It was interrupted again.
  3. Thread 1 returns, and Calendar is no longer the value it set, but the way thread 2 designed it. If you have multiple threads competing for calendar objects at the same time, you can have all sorts of problems, from the wrong time to threads hanging.

Analyzing the implementation of format, it is not difficult to find that the only advantage of using the member variable Calendar is that when subFormat is called, a parameter is missing, but it brings many problems. In fact, if you just take a local variable here, pass it along, everything will be solved.

Behind this problem lies a more important one: one of the benefits of stateless methods is that they can be safely called in a variety of circumstances. The measure of whether a method is stateful is whether it modifies other things, such as global variables, such as instance fields. The format method changes the SimpleDateFormat Calendar field as it runs, so it is stateful. This also reminds us of the following three points when developing and designing systems:

  • 1. When writing your own public class, specify the consequences of multithreaded calls in comments;
  • 2. In the threading environment, pay attention to the thread safety of each shared variable;
  • 3. Our classes and methods should be designed as stateless as possible;

3. Solutions

1. Create new instances when necessary

package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { public static String formatDate(Date date)throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } public static Date parse(String strDate) throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(strDate); }}Copy the code

Note: Create a new instance where SimpleDateFormat needs to be used. Whenever a thread-safe object is changed from shared to partially private, it avoids multithreading problems, but also increases the burden of creating the object. In general, the effect on performance is not significant.

2. Use synchronization: Synchronize the SimpleDateFormat object

package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; 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); }}}Copy the code

Note: When there are many threads, when a thread calls this method, other threads that want to call this method will need to block, multithreading concurrency will have a certain impact on performance.

3. Using ThreadLocal

package com.peidasoft.dateformat; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; 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); }}Copy the code

Another way to write it is:

package com.peidasoft.dateformat; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.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); }}Copy the code

Note: Using ThreadLocal is also a way to make shared variables exclusive, and thread exclusivity can certainly reduce the overhead of creating objects in a concurrent environment compared to method exclusivity. This method is recommended for high performance requirements.

4. Ditch the JDK and use time-formatted classes from other libraries

FastDateFormat

SimpleDateFormat uses Apache Commons FastDateFormat, which claims to be fast and thread-safe. Unfortunately, it can only format dates, not parse date strings.

Joda – Time class library

2. Use the Joda-time class library to deal with time-related problems and do a simple stress test. Method 1 is the slowest, method 3 is the fastest, but even the slowest method 1 is not bad, and the general system method 1 and method 2 can satisfy, so it is difficult to become the bottleneck of your system at this point. To keep things simple, I recommend either method one or method two, or method three, ThreadLocal caching, if you’re looking for a bit of a performance boost when necessary. The joda-time library is perfect for handling Time.