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

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

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

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

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

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

Ответ 1

Попробуй.

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

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

Ответ 2

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

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

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

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

Ответ 3

Если вы используете 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

Ответ 4

Грубо говоря, что вы не должны определять переменную экземпляра 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)

Ответ 5

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

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

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

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

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

Ответ 10

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