Problème de concurrence avec SimpleDateFormat

La classe DateUtil utilisée dans mon projet possède de nombreuse méthodes, certaines permettent de créer des identifiants basés sur les dates ensuite insérés en base de données. L’objet simpleDateFormat qui permet le parsage de ces dates est définit comme étant « public static final ».

Dans la plupart des cas, pas de problème, la date est bien parsée et l’identifiant est crée conformément à ce que l’on attends. Néanmoins, dans de rares cas, il est possible que cela se passe beaucoup moins bien. En effet, la classe SimpleDateFormat n’est pas synchronisée. Si 2 threads essaient en même temps d’utiliser cette instance pour un parsage ou un formatage, le résultat est aléatoire.

La classe DateUtil a été simplifiée à l’extrême.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil {

	public static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
			"ddMMyyyy");

	public static final Date parse(String date) throws ParseException{
		return simpleDateFormat.parse(date);
	}

	public static final String format(Date date) throws ParseException{
		return simpleDateFormat.format(date);
	}
}

La classe de test ci dessous fait en sorte que deux threads concurrents effectuent des parsages/formatages via les deux méthodes définies dans la classe DateUtil sur les trois dates de référence définies dans le tableau de String. Elle effectue ensuite une comparaison entre la date de référence et celle qui a été parsée+formatée.

import java.text.ParseException;

public class TestSimpleDateFormat {

  public static void main(String[] args) {
    final String[] tabDateString = new String[] {
        "01012001", "08082008", "05052005" };

    Runnable runnable = new Runnable() {
      public void run() {
        try {
          for (int j = 0; j < 1000; j++) {
            for (int i = 0; i < 2; i++) {
              String date = DateUtil
                  .format(DateUtil
                      .parse(tabDateString[i]));
              if (!(tabDateString[i].equals(date))) {
                throw new ParseException(
                    tabDateString[i] + " =>"
                        + date, 0);
              }
            }
          }
        } catch (ParseException e) {
          e.printStackTrace();
        }

      }
    };
    new Thread(runnable).start();
    Runnable runnable2 = new Runnable() {
      public void run() {
        try {
          for (int j = 0; j < 1000; j++) {
            for (int i = 0; i < 2; i++) {
              String date = DateUtil
                  .format(DateUtil
                      .parse(tabDateString[i]));
              if (!(tabDateString[i].equals(date))) {
                throw new ParseException(
                    tabDateString[i] + " =>"
                        + date, 0);
              }
            }
          }
        } catch (ParseException e) {
          e.printStackTrace();
        }
      }
    };
    new Thread(runnable2).start();
  }

}

Les résultats sont plus que divers, voilà ci dessous la liste des exceptions les plus fréquentes.

java.text.ParseException: 08082008 =>08012008
	at TestSimpleDateFormat$1.run(TestSimpleDateFormat.java:19)
	at java.lang.Thread.run(Thread.java:619)
java.text.ParseException: 01012001 =>08012001
	at TestSimpleDateFormat$2.run(TestSimpleDateFormat.java:42)
	at java.lang.Thread.run(Thread.java:619)
Exception in thread "Thread-1" java.lang.NumberFormatException: For input string: ".808E.8082E2"
	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:1934)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)
	at java.text.DateFormat.parse(DateFormat.java:335)
	at TestSimpleDateFormat$2.run(TestSimpleDateFormat.java:40)
	at java.lang.Thread.run(Thread.java:619)
Exception in thread "Thread-1" 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:1934)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)
	at java.text.DateFormat.parse(DateFormat.java:335)
	at TestSimpleDateFormat$2.run(TestSimpleDateFormat.java:40)
	at java.lang.Thread.run(Thread.java:619)
Exception in thread "Thread-1" 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:1934)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)
	at java.text.DateFormat.parse(DateFormat.java:335)
	at TestSimpleDateFormat$2.run(TestSimpleDateFormat.java:40)
	at java.lang.Thread.run(Thread.java:619)

Bien sur, ces erreurs n’arrivent en réalité qu’exceptionnellement. Parfois, des exceptions sont remontées, parfois cela a l’air de bien se passer mais l’objet crée est en fait une combinaison d’autres dates et ne correspond donc pas à la date attendue. Il y a plusieurs manière de rendre la classe DateUtil thread safe.

Le plus simple, ne pas rendre l’objet simpleDateFormat comme étant un attribut de la classe mais de le recréer à chaque appel.
Ex :

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil {

	public static final Date parse(String date) throws ParseException{
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
		"ddMMyyyy");
		return simpleDateFormat.parse(date);
	}

	public static final String format(Date date) throws ParseException{
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
		"ddMMyyyy");
                return simpleDateFormat.format(date);
	}
}

Dans le cas de méthodes communes, il est possible que cela deviennent couteux.

Une deuxième méthode est la définition des deux méthodes parse & format comme étant synchronized :

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil {

	public static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
			"ddMMyyyy");

	public synchronized static final Date parse(String date) throws ParseException{
		return simpleDateFormat.parse(date);
	}

	public synchronized  static final String format(Date date) throws ParseException{
		return simpleDateFormat.format(date);
	}
}

Cette méthode marche, néanmoins, il est possible qu’elle crée un goulet d’étranglement du au fonctionnement de synchronized, les méthodes format & parse pouvant être extrêmement communes dans une application. Néanmoins, elle offre l’avantage d’être simple et claire.

Une approche un peu plus fine est l’utilisation d’un objet ThreadLocal. Cet objet permet d’utiliser des variables comme étant locale à un thread. Tous les threads ont une copie de la variable.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil {

	private static ThreadLocal format = new ThreadLocal() {
        protected synchronized SimpleDateFormat initialValue() {
            return new SimpleDateFormat(
			"ddMMyyyy");
        }
    };

    private static SimpleDateFormat getSimpleDateFormat(){
    	return format.get();
    }

	public  static final Date parse(String date) throws ParseException{
		return getSimpleDateFormat().parse(date);
	}

	public  static final String format(Date date) throws ParseException{
		return getSimpleDateFormat().format(date);
		}
}

Comme je suis dans le cas où ma classe DateUtil n’a besoin que d’une méthode format, j’ai choisi une 4ème méthode, l’utilisation non plus de l’objet java.text.SimpleDateFormat mais org.apache.commons.lang.time.FastDateFormat . Le formatage est supportée, avec le même pattern que pour la classe SimpleDateFormat à l’exception du z (se référer à la doc).

8 Responses to “Problème de concurrence avec SimpleDateFormat”

  1. Jérôme Ruillier dit :

    Merci pour cet article !

    Grâce a vous je découvre ThreadLocal qui me sera probablement bien utile à l’avenir !

  2. Excellent, je me suis posé la même question aujourd’hui à propos des SimpleDateFormat ! Mais pris par le temps, j’ai opté pour la solution la plus simple :/
    Merci pour les infos !

  3. Sylvain V. dit :

    Effectivement, il faut parfois se méfier des classes du frameworks… A cause (ou grace) aux serveurs d’application, les développeur oublient facilement qu’ils sont de facto dans un environnement multithreadé.

    Note : sur http://java.sun.com/j2se/1.4.2/docs/api/java/text/SimpleDateFormat.html

    il est bien indiqué :

    « Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally. »

    …la question étant plutôt : quel est le coût de création d’un SimpleDateFormat et est-il appelé très régulièrement ?

    Il me semble que le précalculer en static « par défaut » rentre dans la « Premature optimization ». Qu’en pensez-vous ?

  4. Mathilde dit :

    C’est un peu couteux, du justement au calendar.

    Mais je pense également que dans la plupart des cas c’est effectivement prématuré !

    Il y a énormément de code sur Internet avec le date format en static :(

  5. David Gageot dit :

    La création d’un DateFormat à chaque formatage de date est un dans le top 10 des problèmes de performance les plus courants. C’est incroyable de voir que la plupart des développeurs n’ont aucune idée du coût de ce type d’opération. Du code faisant plusieurs transformations/formatage de dates peut vite devenir extrêmement lent.

    Pour moi, un DateFormat threadsafe en variable statique est la meilleure solution. Clair, rapide, sûr. Seulement, il faut chercher ailleurs que dans le JDK. Par exemple dans la librairie JodaTime.

  6. Jerome Baumgarten dit :

    Comme David, je conseille fortement JodaTime, d’autant plus que JodaTime a servi de base a la JSR 310 (http://jcp.org/en/jsr/detail?id=310) dont l’un des specs leaders n’est autre que Stephen Colebourne, le createur de JodaTime. Avec JodaTime on n’a pas de probleme de concurrence : DateTimeFormat is thread-safe and immutable, and the formatters it returns are as well. De plus DateTimeFormat utilise un cache en interne pour ne pas avoir a construire un DateTimeFormatter a chaque demande.

  7. Je viens de m’amuser à comparer :
    (a) un test de perf avec SimpleDateFormat protégé (non pas par « synchronized ») mais par un ReentrantLock
    (b) un test de perf avec FastDateFormat.

    J’ai créé des tests simples, mais en faisant quand même un peu attention au fait de « chauffer » la JVM (java version « 1.6.0_21 »)

    Et bien, FastDateFormat n’est pas si rapide que cela.

    Le test (a) est plus rapide que (b)…

  8. DDP dit :

    Pour moi, le mieux est d’initialiser toutes les dates en XML

Leave a Reply