Pages

mardi 23 avril 2013

La notion de temps en Java avec System.currentTimeMillis() et System.nanotime()

Comportement de System.currentTimeMillis()

Qu’obtiens t’on quand on exécute ?

   System.out.println(System.currentTimeMillis());
   Thread.currentThread().sleep(1);
   System.out.println(System.currentTimeMillis());

Le résultat est
   1365436257623
   1365436257623

Bizarre bizarre on retente en introduisant une boucle pour vérifier le résultat sur une minute

   for(int i=0 ; i<1000 ; i++){
       System.out.println(System.currentTimeMillis());
       Thread.currentThread().sleep(1);
   }

Je ne vais pas donner toute la trace mais elle est similaire à

   1365436715292
   1365436715292
   1365436715292
   1365436715292
   1365436715292
   1365436715292
   1365436715292
   1365436715292
   1365436715308
   1365436715308
   1365436715308
   .....
On constate un changement toutes les x ms où x est souvent 10 mais pas forcément. En fait le retour de la méthode System.currentTimeMillis() dépend du système d’exploitation sur lequel vous exécuter la commande.

Comportement de System.nanoTime()

Depuis Java 5 une nouvelle méthode nanotime() a été ajoutée dans la classe System. Reprenons notre test initial
   for(int i=0 ; i<1000 ; i++){
      System.out.println(System.nanoTime());
      Thread.currentThread().sleep(1);
   }
 la log est la suivante
   142601475743425
   142601477181038
   142601479133800
   142601481086842
   142601483037928
   142601485015833
   142601486945687
   142601488907389
   142601490866855
   142601492805370


Nous obtenons à première vue un résultat plus pertinent.

Est ce que cette méthode nanotime est la solution miracle ?

Eh bien non. N’allez surtout pas remplacer tout vos currentTimeMillis par des nanotime... Vous êtes d’ailleurs alerté dans la javadoc de la méthode

This method can only be used to measure elapsed time and is not related to any other notion of system or wall-clock time....This method provides nanosecond precision, but not necessarily nanosecond resolution (that is, how frequently the value changes) - no guarantees are made except that the resolution is at least as good as that of currentTimeMillis().

La valeur retournée par cette méthode représente le nombre de nanosecondes calculé depuis une date fixe mais dont l’origine change. Vous aurez la même origine dans une même instance de la JVM mais ce ne sera potentiellement pas la même dans une autre.

De plus la valeur retournée est d’une précision à la nanoseconde mais ne représente pas la date courante!

Un exemple

   System.out.println(new Date(System.currentTimeMillis()));
   System.out.println(new Date(System.nanoTime()/1000));

donnera
   Mon Apr 22 14:20:49 CEST 2013
   Tue Nov 19 00:25:18 CET 1974

Il y a deux méthodes pour récupérer un temps car nous avons deux besoins différents

currentTimeMillis ou nanoTime ?

Dans un système nous pouvons avoir deux besoins liés au date
  • lire une valeur de temps, et
  • programmer / déclencher un événement lié au temps
Le terme clock (littéralement horloge) est souvent utilisé pour se référer à ces deux notions parce que le temps est lu sur une horloge, et une alerte se déclenche quand l'horloge atteint un certain temps. On limitera plutôt le terme clock au premier cas et on parlera de Timer (littéralement minuterie) pour le second.

Le temps est lu généralement à la milliseconde. Les évènements liés au temps sont déclenchés par des interruptions au niveau OS qui se font généralement toutes les 10 ms (il faut bien laisser du temps aux autres opérations lancées). Ce délai est parfois paramétrable au niveau OS et il est le delta observé plus haut dans l’appel successif de la fonction currentTimeMillis.

Lorsque l’on souhaite utiliser un timer ou calculer un temps d’exécution on aurait besoin d’une horloge donnant le temps à la microsecondes et non pas toutes les 10ms. C’est pourquoi les systèmes ont souvent une résolution différente de l’horloge absolue (time-of-day clock) et de l’heure relative d’exécution (free-running relative clock).

La méthode nanoTime utilise l’horloge ayant la plus haute résolution sur une plateforme et même si la valeur retournée est en nanoseconde, la fréquence de mise à jour elle se fait toutes les millisecondes. A noter que sur certains anciens systèmes (vielles versions de Linux ou Windows 98), le temps relatif est défini à partir du temps absolu (mis à jour toutes les 10ms). On peut avoir des différences de comportement très importantes d’un système à un autre et on ne doit ni incriminer la VM ni le système d’exploitation mais plutôt le hardware calculant les temps.

Implementation de ces méthodes

Si on regarde la signature de ces méthodes dans la classe System nous avons

   public static native long currentTimeMillis();
   public static native long nanoTime();

On peut observer le modifier native de ces méthodes que nous n’utilisons pas souvent dans nos applications de gestion. Il permet de définir l’appel à une méthode qui n’est pas incluse dans la JVM. En gros l’API JNI (Java Native Interface) intégrée dans la JVM permet au code Java s’exécutant dans la VM de faire appel ou d’être appelé par des applications natives. Je ne suis pas un expert de cette API mais pour les plus interessés j’ai laissé en référence le lien vers la doc Oracle.

Conclusion

On utilise souvent les fonctions de temps en Java sans vraiment se soucier de ce qu’il y a derrière. Pour ma part ce n’est qu’une différence de comportement du timer swing en fonction de la machine et de la version de Java qui m’a amené à creuser le sujet. Pour conclure cet article il faut garder à l’esprit que la méthode System.currentTimeMillis() doit être utilisé lorsque vous voulez récupérer une date, une heure. Par contre pour un timer ou un calcul de temps d’exécution il faut utiliser System.nanoTime()


Référence

2 commentaires:

  1. Avec la méthode nanoTime(), le résultat n'est quand même pas entièrement satisfaisant. En effet, 1ms = 1.000.000ns et pourtant dans le log quand on fait la différence entre 2 termes consécutifs, on trouve 1.437.613 et 1.952.762 en ne considérant que les 3 premières valeurs.

    RépondreSupprimer
    Réponses
    1. En fonction du système on peut avoir des différences de comportement car cette méthode ne fait que référence à une méthode de très bas niveau. Lorsque l'on fait un compteur cette méthode sera toujours plus précise que currentTimeMillis. Pour avoir des données plus parlante il faudrait lorsque vous faites la différence oublier la partie décimale.
      En gros
      Long oldValue = System.nanoTime();
      for(int i=0 ; i<1000 ; i++){
      Long newValue = System.nanoTime();
      System.out.println(int((newValue-oldValue)/1000000));
      oldValue = newValue;
      Thread.currentThread().sleep(1);
      }
      Le taux d'erreur est très bas et sur mes tests l'erreur est d'une milliseconde. J'ai du mal à voir les cas d'application une erreur d'1ms pourrait être problématique.

      Supprimer

Remarque : Seul un membre de ce blog est autorisé à enregistrer un commentaire.