Java DateFormat не является потокобезопасным, к чему это приводит?

Все предупреждают, что Java DateFormat не является потокобезопасным, и я теоретически понимаю эту концепцию.

Но я не могу представить, с какими реальными проблемами мы можем столкнуться из-за этого. Скажем, у меня есть поле DateFormat в классе, и оно же используется в разных методах класса (форматирование дат) в многопоточной среде.

Будет ли это причиной:

  • любое исключение, такое как исключение формата
  • расхождение в данных
  • любая другая проблема?

Кроме того, пожалуйста, объясните, почему.


person haps10    schedule 26.10.2010    source источник
comment
Вот к чему это приводит: stackoverflow.com/questions/14309607/   -  person caw    schedule 14.01.2013
comment
Сейчас 2020 год. Выполнение моих тестов (параллельно) обнаружило, что дата из одного потока случайно возвращается, когда другой поток пытается отформатировать дату. Мне потребовалось пару недель, чтобы выяснить, от чего это зависит, пока не обнаружил в средстве форматирования, что конструктор создает экземпляр календаря, а календарь позже настраивается на дату, которую мы форматируем. У них в голове все еще 1990 год? Кто знает.   -  person Vlad Patryshev    schedule 23.05.2020
comment
@VladPatryshev В 2020 году вам больше не следует использовать классы DateFormat, SimpleDateFormat, Date и Calendar. Несколько лет назад они были вытеснены современными классами java.time, определенными в JSR 310. Классы java.time — это потокобезопасный по дизайну с использованием неизменяемые объекты.   -  person Basil Bourque    schedule 21.03.2021
comment
@BasilBourque Это был очень важный комментарий, не шутка. Я лишь смутно осознавал, что мне нужно куда-то мигрировать.   -  person Vlad Patryshev    schedule 16.04.2021


Ответы (11)


Давайте попробуем.

Вот программа, в которой несколько потоков используют общий SimpleDateFormat.

Программа:

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

Запустите это несколько раз, и вы увидите:

Исключения:

Вот несколько примеров:

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)

Неверные результаты:

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

Правильные результаты:

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

Другой подход к безопасному использованию DateFormats в многопоточной среде заключается в использовании переменной ThreadLocal для хранения объекта DateFormat, что означает, что каждый поток будет иметь свою собственную копию и не должен ждать, пока другие потоки ее освободят. Вот как:

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

Вот хороший пост с более подробной информацией.

person dogbane    schedule 26.10.2010
comment
Я думаю, причина, по которой это так расстраивает разработчиков, заключается в том, что на первый взгляд кажется, что это должен быть «функционально ориентированный» вызов функции. Например. для одного и того же ввода я ожидаю одного и того же вывода (даже если его вызывают несколько потоков). Я считаю, что ответ сводится к тому, что разработчики Java не ценили FOP в то время, когда они писали исходную логику даты и времени. Итак, в конце концов, мы просто говорим, что нет причин, почему это так, кроме того, что это просто неправильно. - person Lezorte; 20.09.2019

Я бы ожидал повреждения данных - например. если вы анализируете две даты одновременно, у вас может быть один вызов, загрязненный данными из другого.

Легко представить, как это могло произойти: синтаксический анализ часто включает в себя сохранение определенного состояния относительно того, что вы уже прочитали. Если два потока используют одно и то же состояние, у вас возникнут проблемы. Например, DateFormat предоставляет поле calendar типа Calendar, и глядя на код SimpleDateFormat, некоторые методы вызывают calendar.set(...), а другие вызывают calendar.get(...). Это явно не потокобезопасно.

Я не изучал точные детали того, почему DateFormat не является потокобезопасным, но для меня достаточно знать, что он небезопасен без синхронизации - точные способы небезопасных может даже измениться между выпусками.

Лично я бы использовал синтаксические анализаторы из Joda Time, поскольку они потокобезопасны, а Joda Time гораздо лучший API даты и времени для начала :)

person Jon Skeet    schedule 26.10.2010
comment
+1 jodatime и сонар для обеспечения его использования: mestachs.wordpress.com/2012/03/17/ - person mestachs; 12.09.2012


Грубо говоря, вы не должны определять DateFormat как переменную экземпляра объекта, к которому обращаются многие потоки, или static.

Форматы даты не синхронизируются. Рекомендуется создавать отдельные экземпляры формата для каждого потока.

Итак, если к вашему Foo.handleBar(..) обращаются несколько потоков, вместо:

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

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

вы должны использовать:

public class Foo {

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

Кроме того, ни в коем случае не используйте static DateFormat

Как отметил Джон Скит, у вас могут быть как статические, так и общие переменные экземпляра на случай, если вы выполняете внешнюю синхронизацию (т. е. используете synchronized вокруг вызовов DateFormat).

person Bozho    schedule 26.10.2010
comment
Я вообще не вижу, что из этого следует. Я не делаю большинство своих типов потокобезопасными, поэтому я не ожидаю, что их переменные экземпляра также обязательно будут потокобезопасными. Разумнее сказать, что вы не должны хранить DateFormat в переменной static, иначе вам потребуется синхронизация. - person Jon Skeet; 26.10.2010
comment
Как правило, это лучше, хотя было бы нормально иметь статический формат даты, если вы выполнили синхронизацию. Во многих случаях это может работать лучше, чем очень частое создание нового SimpleDateFormat. Это будет зависеть от схемы использования. - person Jon Skeet; 26.10.2010
comment
Не могли бы вы объяснить, как и почему статический экземпляр может вызывать проблемы в многопоточной среде? - person Alexandr; 12.10.2012
comment
потому что он хранит промежуточные вычисления в переменных экземпляра, и это не потокобезопасно - person Bozho; 12.10.2012

Форматы даты не синхронизируются. Рекомендуется создавать отдельные экземпляры формата для каждого потока. Если несколько потоков одновременно обращаются к формату, он должен быть синхронизирован извне.

Это означает, что у вас есть объект DateFormat, и вы обращаетесь к одному и тому же объекту из двух разных потоков, и вы вызываете метод форматирования для этого объекта, оба потока будут входить в один и тот же метод одновременно для одного и того же объекта, чтобы вы могли визуализировать его победу не приводит к надлежащему результату

Если вам нужно работать с DateFormat каким-либо образом, вы должны что-то сделать

public synchronized myFormat(){
// call here actual format method
}
person jmj    schedule 26.10.2010

В лучшем ответе dogbane привел пример использования функции parse и к чему это приводит. Ниже приведен код, позволяющий проверить функцию format.

Обратите внимание, что если вы измените количество исполнителей (одновременных потоков), вы получите другие результаты. Из моих экспериментов:

  • Оставьте newFixedThreadPool равным 5, и цикл каждый раз будет давать сбой.
  • Установите значение 1, и цикл будет работать всегда (очевидно, поскольку все задачи фактически выполняются одна за другой)
  • Установите на 2, и вероятность того, что петля сработает, составляет всего около 6%.

Я предполагаю, что YMMV зависит от вашего процессора.

Функция format дает сбой из-за форматирования времени из другого потока. Это связано с тем, что внутри функции format используется объект calendar, который устанавливается в начале функции format. А объект calendar является свойством класса SimpleDateFormat. Вздох...

/**
 * 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");
}
person Nux    schedule 12.12.2017

Данные повреждены. Вчера я заметил это в своей многопоточной программе, где у меня был статический объект DateFormat, и я вызывал его format() для значений, считанных через JDBC. У меня был оператор выбора SQL, в котором я читал одну и ту же дату с разными именами (SELECT date_from, date_from AS date_from1 ...). Такие операторы использовались в 5 потоках для разных дат в WHERE clasue. Даты выглядели «нормально», но они различались по значению — в то время как все даты были из одного и того же года, изменились только месяц и день.

Другие ответы показывают вам, как избежать такой коррупции. Я сделал свой DateFormat не статическим, теперь он является членом класса, который вызывает операторы SQL. Я тестировал также статическую версию с синхронизацией. Оба работали хорошо, без разницы в производительности.

person Michał Niklas    schedule 26.10.2010

Спецификации Format, NumberFormat, DateFormat, MessageFormat и т. д. не предназначены для потокобезопасности. Кроме того, метод синтаксического анализа вызывает метод Calendar.clone(), и он влияет на элементы календаря, поэтому одновременное выполнение синтаксического анализа большого количества потоков изменит клонирование экземпляра календаря.

Кроме того, это отчеты об ошибках, такие как это и это с результатами проблемы с потокобезопасностью DateFormat.

person Buhake Sindi    schedule 26.10.2010

Если несколько потоков манипулируют/обращаются к одному экземпляру DateFormat и синхронизация не используется, можно получить зашифрованные результаты. Это связано с тем, что несколько неатомарных операций могут непоследовательно изменять состояние или просматривать память.

person seand    schedule 26.10.2010

Это мой простой код, который показывает, что DateFormat не является потокобезопасным.

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

Поскольку все потоки используют один и тот же объект SimpleDateFormat, возникает следующее исключение.

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)

Но если мы передаем разные объекты в разные потоки, код выполняется без ошибок.

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

Это результаты.

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
person Erangad    schedule 19.07.2017
comment
ОП спросил, почему это происходит и что. - person Adam; 09.10.2017

Это приведет к ArrayIndexOutOfBoundsException

Помимо неправильного результата, это время от времени будет вызывать сбой. Это зависит от скорости вашей машины; в моем ноутбуке это происходит в среднем один раз на 100 000 вызовов:

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

последняя строка должна вызвать отложенное исключение исполнителя:

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)
person epox    schedule 11.07.2020