«Java DateFormat n'est pas threadsafe» à quoi cela mène-t-il?


143

Tout le monde met en garde contre le fait que Java DateFormat n'est pas thread-safe et je comprends le concept en théorie.

Mais je ne suis pas en mesure de visualiser les problèmes réels auxquels nous pouvons faire face à cause de cela. Dites, j'ai un champ DateFormat dans une classe et le même est utilisé dans différentes méthodes de la classe (formatage des dates) dans un environnement multi-thread.

Cela causera-t-il:

  • toute exception comme l'exception de format
  • écart dans les données
  • un autre problème?

Veuillez également expliquer pourquoi.


1
Voici ce que cela mène à: stackoverflow.com/questions/14309607/…
caw

Nous sommes en 2020 maintenant. L'exécution de mes tests (en parallèle) a découvert qu'une date d'un thread est retournée avec désinvolture lorsqu'un autre thread tente de formater une date. Cela m'a pris quelques semaines pour enquêter sur ce que cela dépend, jusqu'à ce que trouvé dans un formateur qu'un constructeur instancie un calendrier, et le calendrier est ensuite configuré pour prendre la date que nous formater. Est-ce encore 1990 dans leur tête? Qui sait.
Vlad Patryshev

Réponses:


264

Essayons-le.

Voici un programme dans lequel plusieurs threads utilisent un fichier shared SimpleDateFormat.

Programme :

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

Exécutez ceci plusieurs fois et vous verrez:

Exceptions :

Voici quelques exemples:

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)

Résultats incorrects :

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

Résultats corrects :

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

Une autre approche pour utiliser en toute sécurité DateFormats dans un environnement multi-thread consiste à utiliser une ThreadLocalvariable pour contenir l' DateFormat objet, ce qui signifie que chaque thread aura sa propre copie et n'a pas besoin d'attendre que d'autres threads le libèrent. C'est ainsi:

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

Voici un bon article avec plus de détails.


1
J'adore cette réponse :-)
Sundararaj Govindasamy

Je pense que la raison pour laquelle cela est si frustrant pour les développeurs est qu'à première vue, il semble que cela devrait être un appel de fonction «fonctionnellement orienté». Par exemple, pour la même entrée, j'attends la même sortie (même si plusieurs threads l'appellent). La réponse, je crois, revient au fait que les développeurs de Java n'ont pas une appréciation pour FOP au moment où ils ont écrit la logique de date et d'heure d'origine. Donc à la fin, nous disons simplement "il n'y a aucune raison pour laquelle c'est comme ça autre que c'est juste faux".
Lezorte

30

Je m'attendrais à une corruption des données - par exemple, si vous analysez deux dates en même temps, vous pourriez avoir un appel pollué par les données d'un autre.

Il est facile d'imaginer comment cela pourrait se produire: l'analyse consiste souvent à maintenir un certain état de ce que vous avez lu jusqu'à présent. Si deux threads piétinent tous les deux le même état, vous aurez des problèmes. Par exemple, DateFormatexpose un calendarchamp de type Calendar, et en regardant le code de SimpleDateFormat, certaines méthodes appellent calendar.set(...)et d'autres appellent calendar.get(...). Ce n'est clairement pas sûr pour les threads.

Je n'ai pas examiné les détails exacts de la raison pour laquelle les DateFormatthreads ne sont pas sécurisés, mais pour moi, il suffit de savoir que ce n'est pas sûr sans synchronisation - les manières exactes de non-sécurité pourraient même changer entre les versions.

Personnellement, j'utiliserais plutôt les analyseurs de Joda Time , car ils sont thread-safe - et Joda Time est une bien meilleure API de date et d'heure pour commencer :)


1
+1 jodatime et sonar pour appliquer son utilisation: mestachs.wordpress.com/2012/03/17/…
mestachs

18

Si vous utilisez Java 8, vous pouvez utiliser DateTimeFormatter.

Un formateur créé à partir d'un modèle peut être utilisé autant de fois que nécessaire, il est immuable et est thread-safe.

Code:

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

Production:

2017-04-17

10

En gros, vous ne devez pas définir une DateFormatvariable comme instance d'un objet auquel accèdent de nombreux threads, ou static.

Les formats de date ne sont pas synchronisés. Il est recommandé de créer des instances de format distinctes pour chaque thread.

Donc, dans le cas où vous Foo.handleBar(..)êtes accédé par plusieurs threads, au lieu de:

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

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

Tu devrais utiliser:

public class Foo {

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

De plus, dans tous les cas, ne disposez pas static DateFormat

Comme indiqué par Jon Skeet, vous pouvez avoir à la fois des variables d'instance statiques et partagées au cas où vous effectuez une synchronisation externe (c'est-à-dire à utiliser synchronizedautour des appels à la DateFormat)


2
Je ne vois pas du tout cela qui suit. Je ne fais pas la plupart de mes types thread-safe, donc je ne m'attends pas à ce que leurs variables d'instance soient thread-safe non plus, nécessairement. Il est plus raisonnable de dire que vous ne devriez pas stocker un DateFormat dans un variable statique - ou si vous le faites, vous aurez besoin d'une synchronisation.
Jon Skeet

1
C'est généralement mieux - bien qu'il soit acceptable d'avoir un DateFormat statique si vous avez Synchronize. Cela peut bien fonctionner mieux dans de nombreux cas que de créer un nouveau SimpleDateFormattrès fréquemment. Cela dépendra du modèle d'utilisation.
Jon Skeet

1
Pouvez-vous expliquer comment et pourquoi une instance statique peut causer des problèmes dans un environnement multi-thread?
Alexandr

4
car il stocke les calculs intermédiaires dans les variables d'instance, et ce n'est pas thread-safe
Bozho

2

Les formats de date ne sont pas synchronisés. Il est recommandé de créer des instances de format distinctes pour chaque thread. Si plusieurs threads accèdent à un format simultanément, il doit être synchronisé en externe.

Cela signifie que vous avez un objet DateFormat et que vous accédez au même objet à partir de deux threads différents et que vous appelez la méthode de format sur cet objet, les deux threads entreront sur la même méthode en même temps sur le même objet afin que vous puissiez le visualiser gagné ne résultera pas en un résultat correct

Si vous devez travailler avec DateFormat de quelque manière que ce soit, vous devez faire quelque chose

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

1

Les données sont corrompues. Hier, je l'ai remarqué dans mon programme multithread où j'avais un DateFormatobjet statique et l' ai appelé format()pour les valeurs lues via JDBC. J'ai eu l'instruction de sélection SQL où j'ai lu la même date avec des noms différents ( SELECT date_from, date_from AS date_from1 ...). Ces déclarations étaient utilisées dans 5 threads pour différentes dates en WHEREclasue. Les dates semblaient «normales» mais leur valeur différait - alors que toutes les dates étaient de la même année, seuls le mois et le jour changeaient.

D'autres réponses vous montrent le moyen d'éviter une telle corruption. J'ai fait mon DateFormatpas statique, maintenant c'est un membre d'une classe qui appelle des instructions SQL. J'ai testé aussi la version statique avec synchronisation. Les deux ont bien fonctionné sans aucune différence de performance.


1

Les spécifications de Format, NumberFormat, DateFormat, MessageFormat, etc. n'ont pas été conçues pour être thread-safe. En outre, la méthode parse appelle la Calendar.clone()méthode et affecte les empreintes de calendrier, de sorte que de nombreux threads analysés simultanément modifieront le clonage de l'instance de calendrier.

Pour plus, il s'agit de rapports de bogues tels que celui-ci et celui-ci , avec les résultats du problème de sécurité des threads de DateFormat.


1

Dans la meilleure réponse, dogbane a donné un exemple d'utilisation de la parsefonction et ce à quoi elle mène. Voici un code qui vous permet de vérifier la formatfonction.

Notez que si vous modifiez le nombre d'exécuteurs (threads simultanés), vous obtiendrez des résultats différents. De mes expériences:

  • Laissez newFixedThreadPoolla valeur 5 et la boucle échouera à chaque fois.
  • Défini sur 1 et la boucle fonctionnera toujours (évidemment, car toutes les tâches sont en fait exécutées une par une)
  • Réglez sur 2 et la boucle n'a que 6% de chances de fonctionner.

Je suppose que YMMV dépend de votre processeur.

La formatfonction échoue en formatant l'heure à partir d'un thread différent. En effet, la formatfonction interne utilise un calendarobjet qui est configuré au début de la formatfonction. Et l' calendarobjet est une propriété de la SimpleDateFormatclasse. Soupir...

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

Si plusieurs threads manipulent / accèdent à une seule instance de DateFormat et que la synchronisation n'est pas utilisée, il est possible d'obtenir des résultats brouillés. C'est parce que plusieurs opérations non atomiques peuvent changer d'état ou voir la mémoire de manière incohérente.


0

Ceci est mon code simple qui montre que DateFormat n'est pas thread-safe.

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

Étant donné que tous les threads utilisent le même objet SimpleDateFormat, il lève l'exception suivante.

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)

Mais si nous transmettons différents objets à différents threads, le code s'exécute sans erreur.

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

Voici les résultats.

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

Le PO a demandé pourquoi cela se produit ainsi que quoi.
Adam

0

Cela causera ArrayIndexOutOfBoundsException

Mis à part le résultat incorrect, cela vous causera un crash de temps en temps. Cela dépend de la vitesse de votre machine; dans mon ordinateur portable, cela se produit une fois sur 100000 appels en moyenne:

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

la dernière ligne devrait déclencher l'exception de l'exécuteur reporté:

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)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.