"Java DateFormat n'est pas thread-safe" qu'est-ce entraîne?

tout le monde met en garde contre le fait que Java DateFormat n'est pas un thread sûr et je comprends le concept théoriquement.

mais je ne suis pas capable de visualiser quels problèmes réels nous pouvons faire face en raison de cela. Par exemple, j'ai un champ DateFormat dans une classe et le même est utilisé dans différentes méthodes dans la classe (formatage des dates) dans un environnement multi-threadé.

cette cause:

  • toute exception comme exception de format
  • divergence dans les données
  • autre chose?

veuillez également expliquer pourquoi.

124
demandé sur Paŭlo Ebermann 2010-10-26 10:18:39

10 réponses

essayons.

voici un programme dans lequel plusieurs threads utilisent un SimpleDateFormat partagé .

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écuter ce à quelques reprises 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

Des 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é les formats de données dans un environnement multi-threadé est d'utiliser une variable ThreadLocal pour contenir l'objet DateFormat , ce qui signifie que chaque thread aura sa propre copie et ne besoin d'attendre que d'autres fils pour le libérer. Voici comment:

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.

235
répondu dogbane 2015-04-29 09:19:58

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 des données d'un autre.

il est facile d'imaginer comment cela pourrait arriver: Parser implique souvent de maintenir une certaine quantité d'État quant à ce que vous avez lu jusqu'à présent. Si deux fils piétinent le même état, vous aurez des problèmes. Par exemple, DateFormat expose calendar champ de type Calendar , et en regardant le code de SimpleDateFormat , certaines méthodes appellent calendar.set(...) et d'autres calendar.get(...) . Ce n'est clairement pas thread-safe.

Je n'ai pas regardé dans le exact détails de pourquoi DateFormat n'est pas thread-safe, mais pour moi il suffit de savoir que est dangereux sans synchronisation - les manières exactes de non-sécurité pourrait même changer entre les versions.

personnellement, J'utiliserais les parsers de Joda Time à la place, car ils sont thread safe-et Joda Time est une date et heure API beaucoup mieux pour commencer avec:)

29
répondu Jon Skeet 2010-10-26 06:32:08

si vous utilisez Java 8 alors 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);

sortie:

2017-04-17
10
répondu cjungel 2017-04-17 19:43:04

en gros, que vous ne devriez pas définir un DateFormat comme variable d'instance d'un objet auquel on accède par de nombreux threads, ou static .

Les formats de Date

ne sont pas synchronisés. Il est recommandé de créer des instances de format séparées pour chaque thread.

donc, dans le cas où votre Foo.handleBar(..) est 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());  
    }
}

vous devez utiliser:

public class Foo {

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

aussi, dans tous les cas, ne pas avoir un static DateFormat

comme noté par Jon Skeet, vous pouvez avoir à la fois des variables d'instance statiques et partagées dans le cas où vous effectuez une synchronisation externe (i.e. utiliser synchronized autour des appels à la DateFormat )

9
répondu Bozho 2010-10-26 07:00:14
Les formats de Date

ne sont pas synchronisés. Il est recommandé de créer des instances de format séparées pour chaque thread. Si plusieurs threads accèdent à un format simultanément, il doit être synchronisé extérieurement.

cela signifie supposer que vous avez un objet de DateFormat et vous accédez au même objet à partir de deux threads différents et vous appelez méthode de format sur cet objet les deux threads entreront sur la même méthode au même moment sur le même objet pour que vous puissiez le visualiser ne donnera pas un résultat correct

Si vous avez à travailler avec DateFormat importe comment, alors vous devriez faire quelque chose

public synchronized myFormat(){
// call here actual format method
}
2
répondu Jigar Joshi 2010-10-26 06:24:43

les données sont corrompues. Hier, je l'ai remarqué dans mon programme multithread où j'avais l'objet statique DateFormat et appelé son format() pour les valeurs lues via JDBC. J'ai eu la déclaration SQL select où j'ai lu la même date avec des noms différents ( SELECT date_from, date_from AS date_from1 ... ). De telles déclarations étaient utilisées en 5 threads pour diverses dates dans la classe WHERE . Les Dates semblaient "normales", mais leur valeur différait -- alors que toutes les dates étaient de la même année, seul le mois et le jour changeaient.

D'autres réponses vous montrer le chemin pour éviter une telle corruption. J'ai fait mon DateFormat pas statique, maintenant c'est un membre d'une classe qui appelle des déclarations SQL. J'ai aussi testé la version statique avec synchronisation. Les deux ont bien fonctionné sans aucune différence de rendement.

1
répondu Michał Niklas 2010-10-26 06:52:50

les spécifications de Format, NumberFormat, DateFormat, MessageFormat, etc. n'ont pas été conçus pour être sans fil. En outre, la méthode parse fait appel à la méthode Calendar.clone() et affecte les empreintes de calendrier de sorte que de nombreux threads parsing concurremment modifieront le clonage de l'instance de calendrier.

pour plus, ce sont des rapports de bug tels que ce et ce , avec les résultats de la question de sécurité du thread DateFormat.

1
répondu Buhake Sindi 2010-10-26 07:37:30

En la meilleure réponse dogbane a donné un exemple d'utilisation de parse fonction et ce qu'elle entraîne. Ci-dessous est un code que nous allons vérifier format fonction.

notez que si vous changez le nombre d'exécuteurs (threads concurrents), vous obtiendrez des résultats différents. De mes expériences:

  • Quitter newFixedThreadPool 5 et la boucle échoue à chaque fois.
  • Mis à 1 et la boucle sera toujours travail (évidemment puisque toutes les tâches sont effectivement exécutées une par une)
  • fixé à 2 et la boucle n'a qu'environ 6% de chance de fonctionner.

je suppose que YMMV dépend de votre processeur.

la fonction format ne fonctionne pas en formatant le temps à partir d'un thread différent. Cela est dû au fait que la fonction interne format utilise l'objet calendar qui est configuré au début de la fonction format . Et la L'objet calendar est une propriété de la catégorie 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
répondu Nux 2017-12-12 12:30:45

S'il y a plusieurs threads manipulant/accédant à une seule instance 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 pourraient changer d'état ou voir la mémoire de façon incohérente.

0
répondu seand 2010-10-26 06:25:44

C'est mon code simple qui montre que DateFormat n'est pas sûr.

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

puisque tous les threads utilisent le même objet SimpleDateFormat, il lance 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.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.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.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)

mais si nous passons des objets différents à des threads différents, le code s'exécute sans erreurs.

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
répondu Erangad 2017-07-19 11:00:25