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


144

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

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

Это вызовет:

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

Также объясните, почему.


1
Вот к чему это приводит: stackoverflow.com/questions/14309607/…
caw

Сейчас 2020 год. Выполнение моих тестов (параллельно) обнаружило, что дата из одного потока случайно возвращается, когда другой поток пытается отформатировать дату. Мне потребовалось несколько недель, чтобы разобраться в том, от чего это зависит, пока я не обнаружил в средстве форматирования, что конструктор создает экземпляр календаря, а календарь позже настроен на получение даты, которую мы форматируем. В их головах все еще 1990 год? Кто знает.
Влад Патрышев

Ответы:


266

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

Вот программа, в которой несколько потоков используют общий 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;
  }
}

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


Я думаю, что причина того, почему это так расстраивает разработчиков, заключается в том, что на первый взгляд кажется, что это должен быть «функционально ориентированный» вызов функции. Например, для одного и того же ввода я ожидаю того же вывода (даже если его вызывают несколько потоков). Я считаю, что ответ сводится к тому, что разработчики Java не понимали FOP в то время, когда они писали исходную логику даты и времени. В конце концов, мы просто говорим: «Нет причин, почему это так, кроме того, что это просто неправильно».
Lezorte

30

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

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

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

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


1
+1 jodatime и сонар для обеспечения его использования: mestachs.wordpress.com/2012/03/17/…
mestachs

18

Если вы используете Java 8, вы можете использовать DateTimeFormatter.

Средство форматирования, созданное из шаблона, можно использовать сколько угодно раз, оно неизменяемо и ориентировано на многопоточность.

Код:

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

Выход:

2017-04-17

10

Грубо говоря, вы не должны определять 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)


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

1
Как правило, это лучше - хотя было бы нормально иметь статический DateFormat, если бы вы выполняли синхронизацию. Во многих случаях это может работать лучше, чем SimpleDateFormatочень частое создание нового . Это будет зависеть от модели использования.
Джон Скит,

1
Не могли бы вы объяснить, как и почему статический экземпляр может вызывать проблемы в многопоточной среде?
Александр

4
потому что он хранит промежуточные вычисления в переменных экземпляра, и это не является потокобезопасным
Bozho

2

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

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

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

public synchronized myFormat(){
// call here actual format method
}

2

В лучшем ответе 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");
}

1

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

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


1

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

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


0

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


0

Это мой простой код, который показывает, что 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

ОП спросил, почему это происходит и что.
Адам

0

Это вызовет 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)
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.