149

Everybody cautions regarding Java DateFormat not being thread safe and I understand the concept theoretically.

But I'm not able to visualize what actual issues we can face due to this. Say, I've a DateFormat field in a class and the same is used in different methods in the class (formatting dates) in a multi-threaded environment.

Will this cause:

  • any exception like format exception
  • discrepancy in data
  • any other issue?

Also, please explain why.

Paŭlo Ebermann
  • 68,531
  • 18
  • 138
  • 203
haps10
  • 3,316
  • 4
  • 27
  • 37
  • 1
    This is what it leads to: http://stackoverflow.com/questions/14309607/unexpected-side-effects-when-parsing-dates-in-android – caw Jan 14 '13 at 16:31
  • It is 2020 now. Running my tests (in parallel) discovered that a date from one thread is casually returned when another thread is trying to format a date. Took me a couple of weeks to investigate on what it depends, until found in a formatter that a constructor instantiates a calendar, and the calendar is later configured to take the date we format. Is it still 1990 in their heads? Who knows. – Vlad Patryshev May 23 '20 at 14:33
  • 1
    @VladPatryshev In 2020, you should no longer be using `DateFormat`, `SimpleDateFormat`, `Date`, and `Calendar` classes. These were supplanted years ago by the modern *java.time* classes defined in JSR 310. The *java.time* classes are [thread-safe](https://en.wikipedia.org/wiki/Thread_safety) by design, using [immutable objects](https://en.wikipedia.org/wiki/Immutable_object). – Basil Bourque Mar 20 '21 at 23:34
  • @BasilBourque This was a very important comment, no joke. I was only vaguely aware that I have to migrate somewhere. – Vlad Patryshev Apr 16 '21 at 05:30

11 Answers11

268

Let's try it out.

Here is a program in which multiple threads use a shared SimpleDateFormat.

Program:

public static void main(String[] args) throws Exception {

    final DateFormat format = new SimpleDateFormat("yyyyMMdd");

    Callable<Date> task = new Callable<Date>(){
        public Date call() throws Exception {
            return format.parse("20101022");
        }
    };

    //pool with 5 threads
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<Date>> results = new ArrayList<Future<Date>>();

    //perform 10 date conversions
    for(int i = 0 ; i < 10 ; i++){
        results.add(exec.submit(task));
    }
    exec.shutdown();

    //look at the results
    for(Future<Date> result : results){
        System.out.println(result.get());
    }
}

Run this a few times and you will see:

Exceptions:

Here are a few examples:

1.

Caused by: java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
    at java.lang.Long.parseLong(Long.java:431)
    at java.lang.Long.parseLong(Long.java:468)
    at java.text.DigitList.getLong(DigitList.java:177)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1298)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)

2.

Caused by: java.lang.NumberFormatException: For input string: ".10201E.102014E4"
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1224)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)

3.

Caused by: java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1084)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1936)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)

Incorrect Results:

Sat Oct 22 00:00:00 BST 2011
Thu Jan 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Thu Oct 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

Correct Results:

Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

Another approach to safely use DateFormats in a multi-threaded environment is to use a ThreadLocal variable to hold the DateFormat object, which means that each thread will have its own copy and doesn't need to wait for other threads to release it. This is how:

public class DateFormatTest {

  private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyyMMdd");
    }
  };

  public Date convert(String source) throws ParseException{
    Date d = df.get().parse(source);
    return d;
  }
}

Here is a good post with more details.

ctesniere
  • 111
  • 8
dogbane
  • 242,394
  • 72
  • 372
  • 395
  • I think the reason why this is so frustrating to developers is that at first glance, it looks like it should be a 'functionally oriented' function call. E.g. for the same input, I expect the same output (even if multiple threads call it). The answer I believe comes down to the developers of Java not having an appreciation for FOP at the time that they wrote the original date time logic. So in the end, we just say "there's no reason why it's like this other than it's just wrong". – Lezorte Sep 20 '19 at 16:06
30

I would expect data corruption - e.g. if you're parsing two dates at the same time, you could have one call polluted by data from another.

It's easy to imagine how this could happen: parsing often involves maintaining a certain amount of state as to what you've read so far. If two threads are both trampling on the same state, you'll get problems. For example, DateFormat exposes a calendar field of type Calendar, and looking at the code of SimpleDateFormat, some methods call calendar.set(...) and others call calendar.get(...). This is clearly not thread-safe.

I haven't looked into the exact details of why DateFormat isn't thread-safe, but for me it's enough to know that it is unsafe without synchronization - the exact manners of non-safety could even change between releases.

Personally I would use the parsers from Joda Time instead, as they are thread safe - and Joda Time is a much better date and time API to start with :)

Jon Skeet
  • 1,261,211
  • 792
  • 8,724
  • 8,929
  • 1
    +1 jodatime and sonar to enforce its usage : http://mestachs.wordpress.com/2012/03/17/dangerous-can-be-dating-in-java-joda-to-the-rescue/ – mestachs Sep 12 '12 at 17:57
20

If you are using Java 8 then you can use DateTimeFormatter.

A formatter created from a pattern can be used as many times as necessary, it is immutable and is thread-safe.

Code:

LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String text = date.format(formatter);
System.out.println(text);

Output:

2017-04-17
cjungel
  • 3,365
  • 1
  • 21
  • 19
10

Roughly, that you should not define a DateFormat as instance variable of an object accessed by many threads, or static.

Date formats are not synchronized. It is recommended to create separate format instances for each thread.

So, in case your Foo.handleBar(..) is accessed by multiple threads, instead of:

public class Foo {
    private DateFormat df = new SimpleDateFormat("dd/mm/yyyy");

    public void handleBar(Bar bar) {
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

you should use:

public class Foo {

    public void handleBar(Bar bar) {
        DateFormat df = new SimpleDateFormat("dd/mm/yyyy");
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

Also, in all cases, don't have a static DateFormat

As noted by Jon Skeet, you can have both static and a shared instance variables in case you perform external synchronization (i.e. use synchronized around calls to the DateFormat)

Bozho
  • 554,002
  • 136
  • 1,025
  • 1,121
  • 2
    I don't see that that follows at all. I don't make most of my types thread-safe, so I don't expect their instance variables to be thread-safe either, necessarily. It's more reasonable to say that you shouldn't store a DateFormat in a *static* variable - or if you do, you'll need synchronization. – Jon Skeet Oct 26 '10 at 06:26
  • 1
    That's generally better - although it would be okay to have a static DateFormat if you *did* synchronize. That may well perform better in many cases than creating a new `SimpleDateFormat` very frequently. It will depend on the usage pattern. – Jon Skeet Oct 26 '10 at 06:33
  • 1
    Could you please explain how and why static instance can cause problems in a multi-threaded environment? – Alexandr Oct 12 '12 at 08:16
  • 4
    because it stores intermediate calculations in instance variables, and that's not thread-safe – Bozho Oct 12 '12 at 08:24
2

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.

This means suppose you have a object of DateFormat and you are accessing same object from two different threads and you are calling format method upon that object both thread will enter on the same method at the same time on the same object so you can visualize it won't result in proper result

If you have to work with DateFormat any how then you should do something

public synchronized myFormat(){
// call here actual format method
}
jmj
  • 225,392
  • 41
  • 383
  • 426
2

In the best answer dogbane gave an example of using parse function and what it leads to. Below is a code that let's you check format function.

Notice that if you change the number of executors (concurrent threads) you will get different results. From my experiments:

  • Leave newFixedThreadPool set to 5 and the loop will fail every time.
  • Set to 1 and the loop will always work (obviously as all tasks are actually run one by one)
  • Set to 2 and the loop has only about 6% chance of working.

I'm guessing YMMV depending on your processor.

The format function fails by formatting time from a different thread. This is because internally format function is using calendar object which is set up at the start of the format function. And the calendar object is a property of the SimpleDateFormat class. Sigh...

/**
 * Test SimpleDateFormat.format (non) thread-safety.
 *
 * @throws Exception
 */
private static void testFormatterSafety() throws Exception {
    final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    final Calendar calendar1 = new GregorianCalendar(2013,1,28,13,24,56);
    final Calendar calendar2 = new GregorianCalendar(2014,1,28,13,24,56);
    String expected[] = {"2013-02-28 13:24:56", "2014-02-28 13:24:56"};

    Callable<String> task1 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "0#" + format.format(calendar1.getTime());
        }
    };
    Callable<String> task2 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "1#" + format.format(calendar2.getTime());
        }
    };

    //pool with X threads
    // note that using more then CPU-threads will not give you a performance boost
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<String>> results = new ArrayList<>();

    //perform some date conversions
    for (int i = 0; i < 1000; i++) {
        results.add(exec.submit(task1));
        results.add(exec.submit(task2));
    }
    exec.shutdown();

    //look at the results
    for (Future<String> result : results) {
        String answer = result.get();
        String[] split = answer.split("#");
        Integer calendarNo = Integer.parseInt(split[0]);
        String formatted = split[1];
        if (!expected[calendarNo].equals(formatted)) {
            System.out.println("formatted: " + formatted);
            System.out.println("expected: " + expected[calendarNo]);
            System.out.println("answer: " + answer);
            throw new Exception("formatted != expected");
        /**
        } else {
            System.out.println("OK answer: " + answer);
        /**/
        }
    }
    System.out.println("OK: Loop finished");
}
Nux
  • 7,204
  • 5
  • 47
  • 62
1

Data is corrupted. Yesterday I noticed it in my multithread program where I had static DateFormat object and called its format() for values read via JDBC. I had SQL select statement where I read the same date with different names (SELECT date_from, date_from AS date_from1 ...). Such statements were using in 5 threads for various dates in WHERE clasue. Dates looked "normal" but they differed in value -- while all dates were from the same year only month and day changed.

Others answers show you the way to avoid such corruption. I made my DateFormat not static, now it is a member of a class that calls SQL statements. I tested also static version with synchronizing. Both worked well with no difference in performance.

Michał Niklas
  • 48,759
  • 16
  • 62
  • 100
1

The specifications of Format, NumberFormat, DateFormat, MessageFormat, etc. were not designed to be thread-safe. Also, the parse method calls on Calendar.clone() method and it affects calendar footprints so many threads parsing concurrently will change the cloning of the Calendar instance.

For more, these are bug reports such as this and this, with results of DateFormat thread-safety issue.

Buhake Sindi
  • 82,658
  • 26
  • 157
  • 220
0

If there are multiple threads manipulating/accessing a single DateFormat instance and synchronization not used, it's possible to get scrambled results. That's because multiple non-atomic operations could be changing state or seeing memory inconsistently.

seand
  • 5,044
  • 1
  • 21
  • 35
0

This is my simple code that shows DateFormat is not thread safe.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class DateTimeChecker {
    static DateFormat df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
    public static void main(String args[]){
       String target1 = "Thu Sep 28 20:29:30 JST 2000";
       String target2 = "Thu Sep 28 20:29:30 JST 2001";
       String target3 = "Thu Sep 28 20:29:30 JST 2002";
       runThread(target1);
       runThread(target2);
       runThread(target3);
   }
   public static void runThread(String target){
       Runnable myRunnable = new Runnable(){
          public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() + "  " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
     }
}

Since all the threads are using the same SimpleDateFormat object, it throws the following exception.

Exception in thread "Thread-0" Exception in thread "Thread-2" Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)

But if we pass different objects to different threads, the code runs without errors.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class DateTimeChecker {
    static DateFormat df;
    public static void main(String args[]){
       String target1 = "Thu Sep 28 20:29:30 JST 2000";
       String target2 = "Thu Sep 28 20:29:30 JST 2001";
       String target3 = "Thu Sep 28 20:29:30 JST 2002";
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target1, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target2, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target3, df);
   }
   public static void runThread(String target, DateFormat df){
      Runnable myRunnable = new Runnable(){
        public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() + "  " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
   }
}

These are the results.

Thread-0  Thu Sep 28 17:29:30 IST 2000
Thread-2  Sat Sep 28 17:29:30 IST 2002
Thread-1  Fri Sep 28 17:29:30 IST 2001
Erangad
  • 452
  • 4
  • 12
0

This will cause ArrayIndexOutOfBoundsException

Apart from the incorrect result, it will give you a crash from time to time. It depends on the speed your machine; in my laptop, it happens once in 100,000 calls on average:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<?> future1 = executorService.submit(() -> {
  for (int i = 0; i < 99000; i++) {
    sdf.format(Date.from(LocalDate.parse("2019-12-31").atStartOfDay().toInstant(UTC)));
  }
});

executorService.submit(() -> {
  for (int i = 0; i < 99000; i++) {
    sdf.format(Date.from(LocalDate.parse("2020-04-17").atStartOfDay().toInstant(UTC)));
  }
});

future1.get();

the last line sould trigger the postponed executor exception:

java.lang.ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 13
  at java.base/sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
  at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2394)
  at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2309)
  at java.base/java.util.Calendar.complete(Calendar.java:2301)
  at java.base/java.util.Calendar.get(Calendar.java:1856)
  at java.base/java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1150)
  at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:997)
  at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:967)
  at java.base/java.text.DateFormat.format(DateFormat.java:374)
epox
  • 4,537
  • 36
  • 26