web-dev-qa-db-fra.com

"Java DateFormat n'est pas threadsafe" à quoi cela aboutit-il?

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

Mais je ne suis pas en mesure de visualiser les problèmes auxquels nous pouvons être confrontés pour cette raison. Disons que j'ai un champ DateFormat dans une classe et que celui-ci est utilisé dans différentes méthodes de la classe (dates de formatage) dans un environnement multithread.

Est-ce que cela cause:

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

Aussi, s'il vous plaît expliquer pourquoi.

139
haps10

Essayons-le.

Voici un programme dans lequel plusieurs threads utilisent un partage 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 DateFormats en toute sécurité dans un environnement multithread consiste à utiliser une variable ThreadLocal pour contenir l’objet DateFormat, ce qui signifie que chaque thread aura sa propre copie et n’aura pas besoin de le faire. attendez 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 post avec plus de détails.

252
dogbane

Je m'attendrais à la corruption de données - par exemple Si vous analysez deux dates en même temps, un appel peut être pollué par les données d'un autre.

Il est facile d’imaginer comment cela pourrait se produire: l’analyse implique souvent de conserver une certaine quantité d’état quant à ce que vous avez lu jusqu’à présent. Si deux threads piétinent le même état, vous aurez des problèmes. Par exemple, DateFormat expose un champ calendar 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 thread-safe.

Je n'ai pas examiné les détails exacts de la raison pour laquelle DateFormat n'est pas thread-safe, mais pour moi, il suffit de savoir que it est dangereux sans synchronisation - les méthodes exactes de non-sécurité pourraient même changer entre les versions.

Personnellement, je voudrais utiliser les analyseurs syntaxiques de Joda Time , car ils sont thread safe - et Joda Time est une date bien meilleure et le temps API pour commencer :)

29
Jon Skeet

Si vous utilisez Java 8, vous pouvez alors utiliser DateTimeFormatter .

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

Code:

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

Sortie:

2017-04-17
12
cjungel

En gros, vous ne devez pas définir DateFormat comme variable d’instance d’un objet auquel plusieurs threads ont accès, ni 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ù votre Foo.handleBar(..) est accessible 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 pas avoir un staticDateFormat

Comme l'a noté Jon Skeet, vous pouvez avoir des variables d'instance statiques et partagées dans le cas où vous effectuez une synchronisation externe (c'est-à-dire, utilisez synchronized autour des appels à la DateFormat)

10
Bozho

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 simultanément à un format, celui-ci doit être synchronisé en externe.

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

Si vous devez travailler avec DateFormat any, alors vous devriez faire quelque chose

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

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

Pour plus d'informations, il s'agit de rapports de bugs tels que this et this , avec les résultats du problème de sécurité des threads DateFormat.

1
Buhake Sindi

Les données sont corrompues. Hier, je l’ai remarqué dans mon programme multithread où j’avais un objet statique DateFormat et l’appelais format() pour les valeurs lues via JDBC. J'ai eu l'instruction SQL select où je lis la même date avec des noms différents (SELECT date_from, date_from AS date_from1 ...). De telles déclarations utilisaient dans 5 threads pour différentes dates dans WHERE clasue. Les dates semblaient "normales", mais leur valeur différait - toutes les dates datant de la même année, seuls le mois et le jour avaient changé.

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

1
Michał Niklas

Dans la meilleure réponse, dogbane a donné un exemple d'utilisation de la fonction parse et de ses conséquences. Ci-dessous se trouve un code qui vous permet de vérifier la fonction format.

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 newFixedThreadPool réglé sur 5 et la boucle échouera à chaque fois.
  • Réglez sur 1 et la boucle fonctionnera toujours (évidemment, toutes les tâches étant exécutées une par une)
  • Réglé sur 2, la boucle n'a que 6% de chances de fonctionner.

Je devine YMMV en fonction de votre processeur.

La fonction format échoue en formatant l'heure à partir d'un autre thread. En effet, la fonction format en interne utilise l'objet calendar qui est défini au début de la fonction format. Et l'objet calendar est une propriété de la classe SimpleDateFormat. 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");
}
1
Nux

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

0
seand

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, l'exception suivante est levée.

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

Ce sont 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
0
Erangad