"Java DateFormat ist nicht threadsicher" Wozu führt das?


143

Alle warnen davor, dass Java DateFormat nicht threadsicher ist, und ich verstehe das Konzept theoretisch.

Aber ich kann mir nicht vorstellen, mit welchen tatsächlichen Problemen wir aufgrund dessen konfrontiert werden können. Angenommen, ich habe ein DateFormat-Feld in einer Klasse und dasselbe wird in verschiedenen Methoden in der Klasse (Formatieren von Daten) in einer Umgebung mit mehreren Threads verwendet.

Wird dies verursachen:

  • jede Ausnahme wie Formatausnahme
  • Diskrepanz in Daten
  • irgendein anderes Problem?

Erklären Sie bitte auch warum.


1
Dies ist, was es führt zu: stackoverflow.com/questions/14309607/…
caw

Es ist jetzt 2020. Beim Ausführen meiner Tests (parallel) wurde festgestellt, dass ein Datum von einem Thread zufällig zurückgegeben wird, wenn ein anderer Thread versucht, ein Datum zu formatieren. Ich habe ein paar Wochen gebraucht, um zu untersuchen, worauf es ankommt, bis in einem Formatierer festgestellt wurde, dass ein Konstruktor einen Kalender instanziiert, und der Kalender später so konfiguriert wird, dass er das von uns formatierte Datum annimmt. Ist es noch 1990 in ihren Köpfen? Wer weiß.
Vlad Patryshev

Antworten:


262

Probieren wir es aus.

Hier ist ein Programm, in dem mehrere Threads eine gemeinsame Nutzung verwenden SimpleDateFormat.

Programm :

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

Führen Sie dies einige Male aus und Sie werden sehen:

Ausnahmen :

Hier einige Beispiele:

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)

Falsche Ergebnisse :

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

Richtige Ergebnisse :

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

Ein anderer Ansatz zur sicheren Verwendung von DateFormats in einer Umgebung mit mehreren Threads besteht darin, eine ThreadLocalVariable zum Speichern des DateFormat Objekts zu verwenden. Dies bedeutet, dass jeder Thread eine eigene Kopie hat und nicht warten muss, bis andere Threads sie freigeben. Das ist wie:

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

Hier ist ein guter Beitrag mit mehr Details.


1
Ich liebe diese Antwort :-)
Sundararaj Govindasamy

Ich denke, der Grund, warum dies für Entwickler so frustrierend ist, ist, dass es auf den ersten Blick so aussieht, als ob es sich um einen "funktional orientierten" Funktionsaufruf handeln sollte. ZB für dieselbe Eingabe erwarte ich dieselbe Ausgabe (auch wenn mehrere Threads sie aufrufen). Die Antwort, die ich glaube, liegt darin, dass die Entwickler von Java zu dem Zeitpunkt, als sie die ursprüngliche Datums- / Uhrzeitlogik geschrieben haben, keine Wertschätzung für FOP haben. Am Ende sagen wir einfach: "Es gibt keinen Grund, warum es so ist, außer dass es einfach falsch ist."
Lezorte

30

Ich würde eine Datenbeschädigung erwarten - z. B. wenn Sie zwei Daten gleichzeitig analysieren, könnte ein Anruf durch Daten eines anderen verschmutzt werden.

Es ist leicht vorstellbar, wie dies passieren könnte: Beim Parsen muss häufig ein bestimmter Status für das beibehalten werden, was Sie bisher gelesen haben. Wenn zwei Threads im selben Status trampeln, treten Probleme auf. Zum Beispiel wird DateFormatein calendarFeld vom Typ Calendarverfügbar gemacht und der Code SimpleDateFormateiniger Methodenaufrufe calendar.set(...)und anderer Aufrufe betrachtet calendar.get(...). Dies ist eindeutig nicht threadsicher.

Ich habe nicht genau untersucht, warum DateFormates nicht threadsicher ist, aber für mich ist es genug zu wissen, dass es so ist , ohne Synchronisation unsicher - die genaue Art und Weise von nicht-Sicherheit könnte auch zwischen den Versionen ändern.

Persönlich würde ich die Parser aus verwenden Joda Zeit statt, wie sie sind Thread - sicher - und Joda Zeit ist ein viel besseres Datum und Uhrzeit API zu beginnen :)


1
+1 Jodatime und Sonar, um seine Verwendung durchzusetzen: mestachs.wordpress.com/2012/03/17/…
Mestachs

18

Wenn Sie Java 8 verwenden, können Sie verwenden DateTimeFormatter.

Ein aus einem Muster erstellter Formatierer kann so oft wie nötig verwendet werden, ist unveränderlich und threadsicher.

Code:

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

Ausgabe:

2017-04-17

10

Grob gesagt, dass Sie keine DateFormatals Instanzvariable eines Objekts definieren sollten, auf das viele Threads zugreifen, oder static.

Datumsformate werden nicht synchronisiert. Es wird empfohlen, für jeden Thread separate Formatinstanzen zu erstellen.

Für den Fall, dass Foo.handleBar(..)mehrere Threads auf Ihr zugreifen, anstatt:

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

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

du solltest benutzen:

public class Foo {

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

Außerdem haben Sie in allen Fällen keine static DateFormat

Wie von Jon Skeet bemerkt, können Sie sowohl statische als auch gemeinsam genutzte Instanzvariablen haben, falls Sie eine externe Synchronisation durchführen (dh synchronizedum Aufrufe an die DateFormat)


2
Ich sehe nicht, dass das überhaupt folgt. Ich mache die meisten meiner Typen nicht threadsicher, daher erwarte ich nicht unbedingt, dass ihre Instanzvariablen auch threadsicher sind. Es ist vernünftiger zu sagen, dass Sie ein DateFormat nicht in einer statischen Variablen speichern sollten - oder wenn Sie dies tun, müssen Sie synchronisieren.
Jon Skeet

1
Das ist im Allgemeinen besser - obwohl es in Ordnung wäre , eine statische Dateformat zu haben , wenn Sie tat synchronisieren. Das kann in vielen Fällen eine bessere Leistung bringen, als SimpleDateFormatsehr häufig eine neue zu erstellen . Dies hängt vom Verwendungsmuster ab.
Jon Skeet

1
Könnten Sie bitte erklären, wie und warum statische Instanzen in einer Multithread-Umgebung Probleme verursachen können?
Alexandr

4
weil es Zwischenberechnungen in Instanzvariablen speichert, und das ist nicht threadsicher
Bozho

2

Datumsformate werden nicht synchronisiert. Es wird empfohlen, für jeden Thread separate Formatinstanzen zu erstellen. Wenn mehrere Threads gleichzeitig auf ein Format zugreifen, muss es extern synchronisiert werden.

Dies bedeutet, dass Sie ein Objekt von DateFormat haben und von zwei verschiedenen Threads auf dasselbe Objekt zugreifen und die Formatmethode für dieses Objekt aufrufen. Beide Threads geben zur gleichen Zeit dieselbe Methode für dasselbe Objekt ein, damit Sie es als gewonnen visualisieren können führt nicht zu einem korrekten Ergebnis

Wenn Sie mit DateFormat arbeiten müssen, sollten Sie etwas tun

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

1

Daten sind beschädigt. Gestern habe ich es in meinem Multithread-Programm bemerkt, wo ich ein statisches DateFormatObjekt hatte und es format()für Werte aufgerufen habe, die über JDBC gelesen wurden. Ich hatte eine SQL-Select-Anweisung, bei der ich dasselbe Datum mit unterschiedlichen Namen las ( SELECT date_from, date_from AS date_from1 ...). Solche Aussagen wurden in 5 Threads für verschiedene Daten in der WHEREKlasse verwendet. Die Daten sahen "normal" aus, unterschieden sich jedoch im Wert - während alle Daten aus demselben Jahr stammten, änderten sich nur Monat und Tag.

Andere Antworten zeigen Ihnen, wie Sie solche Korruption vermeiden können. Ich habe meine DateFormatnicht statisch gemacht, jetzt ist sie Mitglied einer Klasse, die SQL-Anweisungen aufruft. Ich habe auch statische Version mit Synchronisation getestet. Beide funktionierten gut ohne Leistungsunterschied.


1

Die Spezifikationen von Format, NumberFormat, DateFormat, MessageFormat usw. wurden nicht als threadsicher konzipiert. Außerdem ruft die Analysemethode die Methode auf Calendar.clone()und wirkt sich auf die Kalender-Footprints aus, sodass viele Threads, die gleichzeitig analysiert werden, das Klonen der Kalenderinstanz ändern.

Weitere Informationen sind Fehlerberichte wie dieser und jener mit Ergebnissen des DateFormat-Thread-Sicherheitsproblems.


1

In der besten Antwort gab dogbane ein Beispiel für die Verwendung von parseFunktionen und deren Ursachen . Unten finden Sie einen Code, mit dem Sie die formatFunktion überprüfen können.

Beachten Sie, dass Sie unterschiedliche Ergebnisse erhalten, wenn Sie die Anzahl der Ausführenden (gleichzeitige Threads) ändern. Aus meinen Experimenten:

  • Belassen Sie newFixedThreadPool5, und die Schleife schlägt jedes Mal fehl.
  • Auf 1 gesetzt und die Schleife funktioniert immer (offensichtlich, da alle Aufgaben tatsächlich einzeln ausgeführt werden)
  • Auf 2 gesetzt und die Schleife hat nur eine Chance von ca. 6% zu arbeiten.

Ich vermute YMMV abhängig von Ihrem Prozessor.

Die formatFunktion schlägt fehl, indem die Zeit eines anderen Threads formatiert wird. Dies liegt daran, dass die interne formatFunktion ein calendarObjekt verwendet, das zu Beginn der formatFunktion eingerichtet wurde. Und das calendarObjekt ist eine Eigenschaft der SimpleDateFormatKlasse. Seufzer...

/**
 * 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

Wenn mehrere Threads eine einzelne DateFormat-Instanz bearbeiten / darauf zugreifen und die Synchronisation nicht verwendet wird, können verschlüsselte Ergebnisse erzielt werden. Dies liegt daran, dass mehrere nichtatomare Operationen den Status ändern oder den Speicher inkonsistent sehen können.


0

Dies ist mein einfacher Code, der zeigt, dass DateFormat nicht threadsicher ist.

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

Da alle Threads dasselbe SimpleDateFormat-Objekt verwenden, wird die folgende Ausnahme ausgelöst.

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)

Wenn wir jedoch unterschiedliche Objekte an unterschiedliche Threads übergeben, wird der Code fehlerfrei ausgeführt.

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

Das sind die Ergebnisse.

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

Das OP fragte, warum und was passiert.
Adam

0

Dies wird verursachen ArrayIndexOutOfBoundsException

Abgesehen von dem falschen Ergebnis kommt es von Zeit zu Zeit zu einem Absturz. Dies hängt von der Geschwindigkeit Ihrer Maschine ab. In meinem Laptop passiert es durchschnittlich einmal in 100.000 Anrufen:

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

Die letzte Zeile sollte die verschobene Executor-Ausnahme auslösen:

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)
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.