«Java DateFormat не является потокобезопасным», к чему это приводит? - PullRequest
137 голосов
/ 26 октября 2010

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

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

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

  • любое исключение, например, исключение формата
  • несоответствие в данных
  • есть еще вопросы?

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

Ответы [ 10 ]

249 голосов
/ 26 октября 2010

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

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

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

29 голосов
/ 26 октября 2010

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

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

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

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

10 голосов
/ 17 апреля 2017

Если вы используете 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 голосов
/ 26 октября 2010

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

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

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

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

public synchronized myFormat(){
// call here actual format method
}
1 голос
/ 12 декабря 2017

В лучшем ответе Догбейн привел пример использования функции 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 голос
/ 26 октября 2010

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

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

1 голос
/ 26 октября 2010

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

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

0 голосов
/ 19 июля 2017

Это мой простой код, который показывает, что 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 голосов
/ 26 октября 2010

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

...