(Transfer) About SimpleDateFormat Safe Time Formatting Thread Safety

Presumably everyone is no stranger to SimpleDateFormat. SimpleDateFormat is a very commonly used class in Java, which is used to parse and format date strings, but if used carelessly can lead to very subtle and difficult to debug problems, because DateFormat and SimpleDateFormat classes are not thread-safe Yes, calling the format() and parse() methods in a multithreaded environment should use synchronous code to avoid problems. Let's take a step-by-step in-depth study and understanding of the SimpleDateFormat class through a specific scenario.

  1. Introduction
  We are all excellent programmers, and we all know that we should create as few SimpleDateFormat instances as possible in the program, because creating such an instance requires a lot of cost. In an example of reading database data and exporting it to an excel file, each time a time information is processed, a SimpleDateFormat instance object needs to be created, and then the object is discarded. A large number of objects are created in this way, occupying a lot of memory and jvm space. code show as below:

copy code
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 code

  You may say, OK, then I will create a static simpleDateFormat instance, and then put it into a DateUtil class (below), and use this instance to operate directly when using it, so the problem is solved. The improved code is as follows:

copy code
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 code

  Of course, this method is really nice and will work just fine most of the time. But when you use it in production for a while, you realize the fact that it's not thread-safe. Under normal testing conditions, there is no problem, but once in a production environment under a certain load, the problem comes out. He will appear in various situations, such as incorrect conversion time, such as error reporting, such as thread hanging and so on. Let's look at the test case below, that facts speak:

copy code
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 code
copy code
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 code

  The execution output is as follows:

copy code
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 code

  Description: Thread-1 and Thread-0 reported java.lang.NumberFormatException: multiple points error, directly hung up, did not get up; although Thread-2 did not hang up, but the output time was wrong, for example, the time we entered is : 2013-05-24 06:02:20 , when it will output: Mon May 24 06:02:20 CST 2021 Such a supernatural event.

  2. Cause

  As professional programmers, of course we all know that the overhead of sharing a variable is much less than creating a new variable each time. The above optimized static SimpleDateFormat version, where various paranormal errors occur under concurrent conditions, is because the SimpleDateFormat and DateFormat classes are not thread-safe. The reason why we ignore thread safety is because from the interface provided to us by SimpleDateFormat and DateFormat classes, it is really hard to see how it has anything to do with thread safety. Just at the bottom of the JDK documentation there are the following instructions:

  Date formats in SimpleDateFormat are not synchronous. It is recommended (recommended) to create separate format instances for each thread. If multiple threads access a format at the same time, it must maintain external synchronization.

  JDK原始文档如下:
  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 the real reason why the SimpleDateFormat and DateFormat classes are not thread-safe:

  SimpleDateFormat inherits DateFormat and defines an object of Calendar class with a protected property in DateFormat: calendar. Just because the concept of Calendar is complicated, involving time zone and localization, etc., the implementation of Jdk uses member variables to pass parameters, which causes errors in multi-threading.

  In the format method, there is such a piece of code:

copy 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 code

  The statement calendar.setTime(date) changes the calendar, which is used later (in the subFormat method), and this is the source of the problem. Imagine that in a multi-threaded environment, two threads hold the same instance of SimpleDateFormat and call the format method respectively:
  thread 1 calls the format method and changes the calendar field.
  Interruption is coming.
  Thread 2 starts executing and it also changes the calendar.
  Interrupted again.
  Thread 1 is back. At this time, the calendar is not the value it set, but embarked on the road designed by thread 2. If multiple threads compete for the calendar object at the same time, there will be various problems, the timing is wrong, the thread hangs, and so on.
  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, there is one less parameter, but it brings many problems. In fact, as long as you use a local variable here and pass it all the way, all problems will be solved.
  There is a more important problem hidden behind this problem - stateless: one of the benefits of stateless methods is that it can be safely invoked in various environments. A 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 calendar field of SimpleDateFormat during operation, so it is stateful.

  This also reminds us to pay attention to the following three points when developing and designing the system:

  1. When writing a public class by yourself, clearly explain the consequences in the case of multi-threaded calls in the comments

  2. In the thread environment, pay attention to the thread safety of each shared variable variable

  3. When designing our classes and methods, we should try to design them as stateless as possible.

  3. Solutions

  1. Create a new instance when needed:

copy code
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 code

  Description: Create a new instance where SimpleDateFormat needs to be used. Whenever the object with thread safety problem is changed from shared to local private, multi-threading problems can be avoided, but it also increases the burden of creating objects. Under normal circumstances, this actually affects the performance ratio is not very obvious.

  2. Use synchronization: Synchronize SimpleDateFormat objects

copy code
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 code

  Note: When there are many threads, when one thread calls this method, other threads that want to call this method must be blocked. When the multi-thread concurrency is large, it will have a certain impact on performance.

  3. Use ThreadLocal: 

copy code
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 code

  Another way of writing:

copy code
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 code

  Note: Using ThreadLocal also turns shared variables into exclusive use. Thread exclusive use can definitely reduce the overhead of creating objects in a concurrent environment than method exclusive use. If the performance requirements are relatively high, this method is generally recommended.

  4. Ditch the JDK and use the time formatting classes from other libraries:

  1. Use FastDateFormat in Apache commons, claiming to be SimpleDateFormat that is fast and thread-safe, but unfortunately it can only format dates, but cannot parse date strings.

  2. Use the Joda-Time class library to deal with time-related issues

   

  Do a simple stress test, method 1 is the slowest, method 3 is the fastest, but even the slowest method 1 has good performance. Generally, method 1 and method 2 can be satisfied, so it is difficult to be you at this point. The bottleneck of the system is. From a simple point of view, it is recommended to use method 1 or method 2. If you want a little performance improvement when necessary, you can consider using method 3, using ThreadLocal as a cache.

  The Joda-Time class library is perfect for time processing, and it is recommended to use it.

  References:

  1.http://dreamhead.blogbus.com/logs/215637834.html

  2.http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html

 

Source: http://www.cnblogs.com/peida/archive/2013/05/31/3070790.html

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325132321&siteId=291194637