"Java DateFormat не є безпечним для потоків", до чого це призводить?


143

Всі застерігають, що Java DateFormat не є безпечним для потоків, і я розумію цю концепцію теоретично.

Але я не в змозі уявити, з якими актуальними проблемами ми можемо зіткнутися через це. Скажімо, у мене є поле DateFormat у класі, і те саме використовується в різних методах у класі (форматування дат) у багатопотоковому середовищі.

Чи буде це причиною:

  • будь-який виняток, як виняток формату
  • невідповідність даних
  • будь-яке інше питання?

Також, будь ласка, поясніть чому.


1
До цього і призводить: stackoverflow.com/questions/14309607/…
caw

Зараз 2020 рік. Під час виконання моїх тестів (паралельно) з'ясувалося, що дата з одного потоку випадково повертається, коли інший потік намагається відформатувати дату. Мені знадобилося кілька тижнів, щоб дослідити, від чого це залежить, поки у форматері не виявиться, що конструктор створює екземпляр календаря, а календар згодом налаштовується на прийняття дати, яку ми форматуємо. Це все ще 1990 рік у їхніх головах? Хто знає.
Влад Патришев

Відповіді:


264

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

Ось програма, в якій декілька потоків використовують спільний доступ 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;
  }
}

Ось хороший пост з більш детальною інформацією.


1
Я люблю цю відповідь :-)
Sundararaj Govindasamy

Я думаю, що причина, чому це так засмучує розробників, полягає в тому, що, на перший погляд, схоже, що це повинен бути функціонально орієнтований виклик функції. Наприклад, для одного і того ж входу, я очікую того ж виходу (навіть якщо кілька потоків називають його). Відповідь, на мій погляд, зводиться до розробників Java, які не вдячні за FOP під час написання оригінальної логіки часу на дату. Отже, врешті-решт, ми просто кажемо, що "немає причини, чому це подібне, крім просто неправильного".
Лезорте

30

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

Неважко уявити, як це могло статися: синтаксичний аналіз часто передбачає підтримання певної кількості стану щодо того, що ви читали до цього часу. Якщо обидва потоки обриваються в одному стані, у вас виникнуть проблеми. Наприклад, DateFormatвідкриває calendarполе типу Calendarі, дивлячись на код SimpleDateFormat, одні методи викликають, calendar.set(...)а інші викликають calendar.get(...). Це, очевидно, не є безпечним для потоків.

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

Особисто я хотів би використовувати парсер з Joda часу замість того, щоб , як вони є поточно - і Joda час набагато краще , дата і час API для початку :)


1
+1 jodatime та sonar для забезпечення його використання: 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
тому що він зберігає проміжні обчислення в змінних екземплярів, і це не є безпечним для
потоків

2

Формати дати не синхронізовані. Рекомендується створювати окремі екземпляри формату для кожного потоку. Якщо декілька потоків отримують доступ до формату одночасно, він повинен бути синхронізований зовні.

Це означає, що припустімо, що у вас є об'єкт DateFormat, і ви отримуєте доступ до одного об’єкта з двох різних потоків, і ви викликаєте метод формату на цьому об'єкті, обидва потоки будуть входити в один і той же метод одночасно на тому ж об'єкті, щоб ви могли візуалізувати його виграв не призведе до належного результату

Якщо вам доведеться працювати з DateFormat будь-яким чином, тоді вам слід щось робити

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

1

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

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


1

Характеристики формату, NumberFormat, DateFormat, MessageFormat тощо не були розроблені таким чином, щоб бути безпечними для потоків. Також метод розбору закликає Calendar.clone()метод, і він впливає на сліди календаря, тому багато ниток, що аналізують одночасно, змінять клонування екземпляра календаря.

Докладніше, це звіти про помилки, такі як це та це , з результатами проблеми поточної безпеки DateFormat.


1

У найкращій відповіді 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");
}

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.